Gato/AnimatedAlgorithms.py 0100644 0017676 0017534 00000004717 10014153346 0017154 0 ustar 00schliep catbox 0000305 0050066 ################################################################################
#
# This file is part of Gato (Graph Animation Toolbox)
# You can find more information at
# http://gato.sf.net
#
# file: AnimatedAlgorithms.py
# author: Alexander Schliep (schliep@molgen.mpg.de)
#
# Copyright (C) 1998-2003, Alexander Schliep, Winfried Hochstaettler and
# ZAIK/ZPR, Universitaet zu Koeln
#
# Contact: schliep@molgen.mpg.de, wh@zpr.uni-koeln.de
#
# Information: http://gato.sf.net
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Library General Public
# License as published by the Free Software Foundation; either
# version 2 of the License, or (at your option) any later version.
#
# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Library General Public License for more details.
#
# You should have received a copy of the GNU Library General Public
# License along with this library; if not, write to the Free
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#
#
#
# This file is version $Revision: 1.6 $
# from $Date: 2003/02/12 14:24:56 $
# last change by $Author: schliep $.
#
################################################################################
from GatoGlobals import *
from DataStructures import VertexLabeling, Queue
from AnimatedDataStructures import *
#from GraphDisplay import GraphDisplay
#from Graph import SubGraph
def shortestPath(G,A,s,t):
""" Find a shortest path and return it as a set of edges. If no
path exists, it returns None """
pred = AnimatedVertexLabeling(A)
Q = AnimatedVertexQueue(A)
A.SetAllEdgesColor("black")
for v in G.vertices:
pred[v] = None
Q.Append(s)
while Q.IsNotEmpty() and pred[t] == None:
v = Q.Top()
for w in AnimatedNeighborhood(A,G,v):
if pred[w] == None and w != s:
pred[w] = v
Q.Append(w)
if pred[t] == None: # No augmenting path found
return None
path = []
v = t
while pred[v] != None:
A.SetVertexColor(v,"red")
A.SetEdgeColor(pred[v],v,"red")
path.append((pred[v],v))
v = pred[v]
A.SetVertexColor(v,"red")
return path
Gato/AnimatedDataStructures.py 0100644 0017676 0017534 00000063523 10014153347 0020021 0 ustar 00schliep catbox 0000305 0050066 ################################################################################
#
# This file is part of Gato (Graph Animation Toolbox)
# You can find more information at
# http://gato.sf.net
#
# file: AnimatedDataStructures.py
# author: Alexander Schliep (schliep@molgen.mpg.de)
#
# Copyright (C) 1998-2004, Alexander Schliep, Winfried Hochstaettler and
# ZAIK/ZPR, Universitaet zu Koeln
#
# Contact: schliep@molgen.mpg.de, wh@zpr.uni-koeln.de
#
# Information: http://gato.sf.net
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Library General Public
# License as published by the Free Software Foundation; either
# version 2 of the License, or (at your option) any later version.
#
# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Library General Public License for more details.
#
# You should have received a copy of the GNU Library General Public
# License along with this library; if not, write to the Free
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#
#
#
# This file is version $Revision: 1.47 $
# from $Date: 2004/02/11 18:36:01 $
# last change by $Author: schliep $.
#
################################################################################
from GatoGlobals import *
from DataStructures import VertexLabeling, Queue, Stack, PriorityQueue
from Graph import SubGraph
import copy
class Animator:
""" *Debugging* Text only Animator providing animation functions which
only print to console """
def SetVertexColor(self,v, color):
print "set color of",v," to ",color
def SetEdgeColor(self, tail, head, color):
print "set color of edge (",tail,",", head ,") to ",color
class AnimatedNeighborhood:
""" Visualizes visiting of neighbors by calling the Neighborhood
method of graph for v and allowing to iterate over it, while
coloring (v,w) cTraversedEdge unless (v,w) is colored with
one of the colors in leaveColors.
#Neighborhood = lambda v,a=A,g=G: AnimatedNeighborhood(a,g,v,['red'])
#
#for w in Neighborhood(v):
# doSomething
will color all edges cTraversedEdge unless the edge has been colored
'red' at some point
if a blinkColor is specified the edge will blink
"""
def __init__(self,theAnimator,G,v,leaveColors = [],blinkColor=None):
""" theAnimator will usually be the GraphDisplay(Frame/Toplevel) """
self.Animator = theAnimator
self.nbh = G.Neighborhood(v)
self.v = v
self.leaveColors = leaveColors
self.blinkColor = blinkColor
self.lastEdge = None
self.lastColor = None
self.travColor = "yellow"
self.Animator.SetVertexFrameWidth(self.v,8)
def __getitem__(self, i):
try:
if (self.Animator.GetEdgeColor(self.lastEdge[0],self.lastEdge[1]) == self.travColor):
if (self.lastColor not in self.leaveColors):
self.Animator.SetEdgeColor(self.lastEdge[0],self.lastEdge[1],cTraversedEdge)
else:
self.Animator.SetEdgeColor(self.lastEdge[0],self.lastEdge[1],self.lastColor)
except:
None
if i < len(self.nbh):
self.lastEdge = (self.v,self.nbh[i])
self.lastColor = self.Animator.GetEdgeColor(self.v,self.nbh[i])
self.Animator.SetEdgeColor(self.v,self.nbh[i],self.travColor)
if self.blinkColor != None:
self.Animator.BlinkEdge(self.v,self.nbh[i],self.blinkColor)
return self.nbh[i]
else:
self.Animator.SetVertexFrameWidth(self.v,self.Animator.gVertexFrameWidth)
raise IndexError
def __len__(self):
return len(self.nbh)
class BlinkingNeighborhood:
""" Visualizes visiting blinking (v,w) for all w when iterating over
the Neighborhood
#Neighborhood = lambda v,a=A,g=G: BlinkingNeighborhood(a,g,v,c)
#
#for w in Neighborhood(v):
# doSomething
will blink all edges"""
def __init__(self,theAnimator,G,v,c):
""" theAnimator will usually be the GraphDisplay(Frame/Toplevel) """
self.Animator = theAnimator
self.nbh = G.Neighborhood(v)
self.v = v
self.color = c
def __getitem__(self, i):
if i < len(self.nbh):
self.Animator.BlinkEdge(self.v,self.nbh[i],self.color)
return self.nbh[i]
else:
raise IndexError
def __len__(self):
return len(self.nbh)
class BlinkingTrackLastNeighborhood(BlinkingNeighborhood):
""" Visualizes visiting blinking (v,w) for all w when iterating over
the Neighborhood. It also temporarily keeps the the last blinked
edge grey
#Neighborhood = lambda v,a=A,g=G: BlinkingTrackLastNeighborhood(a,g,v,c,track)
#
#for w in Neighborhood(v):
# doSomething
will blink all edges with color c, the last blinked is tracked with color
track """
old = None
def __init__(self,theAnimator,G,v,c,track="grey"):
BlinkingNeighborhood.__init__(self,theAnimator,G,v,c)
self.trackColor = track
def __getitem__(self, i):
if BlinkingTrackLastNeighborhood.old != None and i < len(self.nbh):
old = BlinkingTrackLastNeighborhood.old
self.Animator.SetEdgeColor(old[0],old[1],old[2])
BlinkingTrackLastNeighborhood.old = (self.v,self.nbh[i],
self.Animator.GetEdgeColor(self.v,self.nbh[i]))
retVal = BlinkingNeighborhood.__getitem__(self,i)
self.Animator.SetEdgeColor(self.v,self.nbh[i],self.trackColor)
return retVal
class BlinkingContainerWrapper:
""" Visualizes iterating over a list of vertices and/or edges by
blinking.
#List = lambda l, a=A: BlinkingContainerWrapper(a,l,color)
#
#for w in List:
# doSomething
"""
def __init__(self, theAnimator, l, color=cOnQueue):
self.Animator = theAnimator
self.list = copy.copy(l)
self.color = color
def __getitem__(self, i):
if i < len(self.list):
item = self.list[i]
if type(item) == type(2): # vertex
self.Animator.BlinkVertex(item,self.color)
else:
self.Animator.BlinkEdge(item[0],item[1],self.color)
return item
else:
raise IndexError
def __len__(self):
return len(self.list)
class ContainerWrapper(BlinkingContainerWrapper):
""" Visualizes iterating over a list of vertices and/or edges by
coloring. If color has changed in the meantime the original
color will not be set again.
#List = lambda l, a=A: ContainerWrapper(a,l,color)
#
#for w in List:
# doSomething
"""
def __init__(self, theAnimator, l, color=cOnQueue):
BlinkingContainerWrapper.__init__(self,theAnimator,l,color)
self.lastitem = None
self.lastcolor = None
def __getitem__(self, i):
if i < len(self.list):
item = self.list[i]
if type(item) == type(2): # vertex
dummy = self.Animator.GetVertexColor(item)
if (self.lastitem != None) and (self.Animator.GetVertexColor(self.lastitem) == self.color):
self.Animator.SetVertexColor(self.lastitem,self.lastcolor)
self.Animator.SetVertexColor(item,self.color)
self.lastcolor = dummy
else:
dummy = self.Animator.GetEdgeColor(item[0],item[1])
if (self.lastitem != None) and (self.Animator.GetEdgeColor(self.lastitem[0],self.lastitem[1]) == self.color):
self.Animator.SetEdgeColor(self.lastitem[0],self.lastitem[1],self.lastcolor)
self.Animator.SetEdgeColor(item[0],item[1],self.color)
self.lastcolor = dummy
self.lastitem = item
return item
else:
raise IndexError
class VisibleVertexLabeling(VertexLabeling):
def __init__(self, theAnimator):
VertexLabeling.__init__(self)
self.A = theAnimator
def __setitem__(self, v, val):
VertexLabeling.__setitem__(self, v, val)
if val == gInfinity:
val = "Infinity"
elif val == -gInfinity:
val = "-Infinity"
self.A.SetVertexAnnotation(v,val)
class AnimatedVertexLabeling(VertexLabeling):
""" Visualizes changes of values of the VertexLabeling
by changing vertex colors appropriately.
E.g.,
#d = AnimatedVertexLabeling(A)
#d[v] = 0
will color v cInitial.
The coloring used for d[v] = val
- cInitial if val = 0,None,gInfinity
- "blue" else """
def __init__(self, theAnimator, initial=0, color="blue"):
""" theAnimator will usually be the GraphDisplay(Frame/Toplevel)
initial is the value to cause coloring in cInitial """
VertexLabeling.__init__(self)
self.Animator = theAnimator
self.initial=initial
self.color = color
def __setitem__(self, v, val):
VertexLabeling.__setitem__(self, v, val)
if val == self.initial or val == None or val == gInfinity:
self.Animator.SetVertexColor(v,cInitial)
else:
self.Animator.SetVertexColor(v,self.color)
class AnimatedSignIndicator:
""" Visualizes sign of vertex or edge:
weight > 0 : green
= 0 : grey
< 0 : red """
def __init__(self,theAnimator):
self.Animator = theAnimator
self.weight = {}
def __setitem__(self, i, val):
self.weight[i] = val
if type(i) == type(2): # vertex
if val>0:
self.Animator.SetVertexColor(i,"green")
elif val<0:
self.Animator.SetVertexColor(i,"red")
else:
self.Animator.SetVertexColor(i,"grey")
else:
if val>0:
self.Animator.SetEdgeColor(i,"green")
elif val<0:
self.Animator.SetEdgeColor(i,"red")
else:
self.Animator.SetEdgeColor(i,"grey")
def __getitem__(self, i):
return self.weight[i]
class AnimatedPotential:
""" Visualizes the potential from 0 (green) to
max (brown) of a vertex. """
def __init__(self,max,theAnimator1,theAnimator2=None):
self.pot = {}
self.max = max
self.colors = ['#00FF00','#11EE00','#22DD00','#33CC00','#44BB00',
'#55AA00','#669900','#778800','#887700','#996600',
'#AA5500','#BB4400','#CC3300']
self.Animator1 = theAnimator1
if theAnimator2 == None:
self.Animator2 = theAnimator1
else:
self.Animator2 = theAnimator2
def __setitem__(self,v,val):
self.pot[v] = val
if val == gInfinity:
self.Animator2.SetVertexAnnotation(v,"Inf")
elif val == -gInfinity:
self.Animator2.SetVertexAnnotation(v,"-Inf")
else:
self.Animator2.SetVertexAnnotation(v,"%d"%val)
if val > self.max:
val = self.max
self.Animator1.SetVertexColor(v,self.colors[(val*(len(self.colors)-1))/self.max])
def __getitem__(self,v):
return self.pot[v]
class BlinkingVertexLabeling(VertexLabeling):
""" Visualizes changes of values of the VertexLabeling
by blinking vertices """
def __init__(self, theAnimator):
""" theAnimator will usually be the GraphDisplay(Frame/Toplevel) """
VertexLabeling.__init__(self)
self.Animator = theAnimator
def __setitem__(self, v, val):
VertexLabeling.__setitem__(self, v, val)
if val == 0:
self.Animator.BlinkVertex(v)
else:
self.Animator.BlinkVertex(v)
class AnimatedVertexQueue(Queue):
""" Visualizes status of vertices in relation to the Queue by
coloring them
- cOnQueue if they are in the queue
- cRemovedFromQueue if they have been on the queue and were
removed """
def __init__(self, theAnimator, colorOn=cOnQueue, colorOff=cRemovedFromQueue):
""" theAnimator will usually be the GraphDisplay(Frame/Toplevel) """
Queue.__init__(self)
self.Animator = theAnimator
self.ColorOn = colorOn
self.ColorOff = colorOff
self.lastRemoved = None
def Append(self,v):
Queue.Append(self,v)
self.Animator.SetVertexColor(v, self.ColorOn)
def Top(self):
v = Queue.Top(self)
self.Animator.SetVertexColor(v, self.ColorOff)
if self.lastRemoved is not None:
self.Animator.SetVertexFrameWidth(self.lastRemoved,self.Animator.gVertexFrameWidth)
self.Animator.SetVertexFrameWidth(v,6)
self.lastRemoved = v
return v
def Clear(self):
for v in self.contents:
self.Animator.SetVertexColor(v, self.ColorOff)
Queue.Clear(self)
if self.lastRemoved is not None:
self.Animator.SetVertexFrameWidth(self.lastRemoved,self.Animator.gVertexFrameWidth)
self.lastRemoved = None
class AnimatedVertexPriorityQueue(PriorityQueue):
""" Visualizes status of vertices in relation to the PriorityQueue by
coloring them
- cOnQueue if they are in the queue
- cRemovedFromQueue if they have been on the queue and were
removed """
def __init__(self, theAnimator, colorOn=cOnQueue, colorOff=cRemovedFromQueue):
""" theAnimator will usually be the GraphDisplay(Frame/Toplevel) """
PriorityQueue.__init__(self)
self.Animator = theAnimator
self.ColorOn = colorOn
self.ColorOff = colorOff
self.lastRemoved = None
def Insert(self,value,sortKey):
PriorityQueue.Insert(self,value,sortKey)
self.Animator.SetVertexColor(value, self.ColorOn)
def DecreaseKey(self,value,newSortKey):
PriorityQueue.DecreaseKey(self,value,newSortKey)
self.Animator.BlinkVertex(value)
def DeleteMin(self):
v = PriorityQueue.DeleteMin(self)
self.Animator.SetVertexColor(v, self.ColorOff)
if self.lastRemoved is not None:
self.Animator.SetVertexFrameWidth(self.lastRemoved,self.Animator.gVertexFrameWidth)
self.Animator.SetVertexFrameWidth(v,6)
self.lastRemoved = v
return v
class AnimatedVertexStack(Stack):
""" Visualizes status of vertices in relation to the Stack by
coloring them
- cOnQueue if they are in the queue
- cRemovedFromQueue if they have been on the queue and were
removed """
def __init__(self, theAnimator, colorOn=cOnQueue, colorOff=cRemovedFromQueue):
""" theAnimator will usually be the GraphDisplay(Frame/Toplevel) """
Stack.__init__(self)
self.Animator = theAnimator
self.ColorOn = colorOn
self.ColorOff = colorOff
self.lastRemoved = None
def Push(self,v):
Stack.Push(self,v)
self.Animator.SetVertexColor(v, self.ColorOn)
def Pop(self):
v = Stack.Pop(self)
self.Animator.SetVertexColor(v, self.ColorOff)
if self.lastRemoved is not None:
self.Animator.SetVertexFrameWidth(self.lastRemoved,self.Animator.gVertexFrameWidth)
self.Animator.SetVertexFrameWidth(v,6)
self.lastRemoved = v
return v
def Clear(self):
for v in self.contents:
self.Animator.SetVertexColor(v, self.ColorOff)
Stack.Clear(self)
if self.lastRemoved is not None:
self.Animator.SetVertexFrameWidth(self.lastRemoved,self.Animator.gVertexFrameWidth)
self.lastRemoved = None
##class AnimatedPriorityQueue(PriorityQueue):
## def __init__(self, theAnimator, color=cVisited):
## """ theAnimator will usually be the GraphDisplay(Frame/Toplevel) """
## self.Animator = theAnimator
## self.color = color
## PriorityQueue.__init__(self)
## def Insert(self,value,sortKey):
## # XXX For compat. to AnimatedVertexSet (yuk)
## PriorityQueue.Insert(self,value,sortKey)
## def DeleteMin(self):
## """ Return and delete minimal value with minimal sortKey from queue. """
## v = PriorityQueue.DeleteMin(self)
## self.Animator.SetVertexColor(v,self.color)
## return v
class AnimatedVertexSet:
""" Visualizes status of vertices in relation to the Set by
coloring them
- cVisited if they have been in the set and were
removed """
def __init__(self, theAnimator, vertexSet=None, color=cVisited):
""" theAnimator will usually be the GraphDisplay(Frame/Toplevel) """
if vertexSet == None:
self.vertices = []
else:
self.vertices = vertexSet
self.Animator = theAnimator
self.color = color
def Set(self, vertexSet):
""" Sets the set equal to a copy of vertexSet """
self.vertices = vertexSet[:]
def Remove(self, v):
self.Animator.SetVertexColor(v,self.color)
self.vertices.remove(v)
def Add(self,v):
""" Add a single vertex v """
self.vertices.append(v)
def IsNotEmpty(self):
return len(self.vertices) > 0
def IsEmpty(self):
return len(self.vertices) == 0
def Contains(self,v):
return v in self.vertices
class AnimatedEdgeSet:
""" Visualizes status of edges in relation to the Set by
coloring them
- 'blue' if they are added to the set
- cVisited if they have been in the set and were
removed """
def __init__(self, theAnimator,edgeSet=None):
""" theAnimator will usually be the GraphDisplay(Frame/Toplevel) """
if edgeSet == None:
self.edges = []
else:
self.edges = edgeSet
self.Animator = theAnimator
def __len__(self):
return len(self.edges)
def __getitem__(self,key):
return self.edges[key]
def Set(self, edgeSet):
""" Sets the set equal to a copy of edgeSet """
self.edges = edgeSet[:]
def AddEdge(self, e):
self.Animator.SetEdgeColor(e[0],e[1],"blue")
self.edges.append(e)
def Remove(self, e):
self.Animator.BlinkEdge(e[0],e[1],cVisited)
self.Animator.SetEdgeColor(e[0],e[1],cVisited)
self.edges.remove(e)
def IsNotEmpty(self):
return len(self.edges) > 0
def Contains(self,e):
return e in self.edges
class AnimatedSubGraph(SubGraph):
""" Visualizes status of vertices and edges in relation to the SubGraph by
coloring them
- color (default is 'blue') if they are added to the SubGraph """
def __init__(self, G, theAnimator, color="blue"):
""" color is used to color vertices and edges in the subgraph.
theAnimator will usually be the GraphDisplay(Frame/Toplevel) """
SubGraph.__init__(self, G)
self.Animator = theAnimator
self.Color = color
def AddVertex(self,v):
try:
SubGraph.AddVertex(self,v)
self.Animator.SetVertexColor(v,self.Color)
self.Animator.DefaultInfo()
except NoSuchVertexError:
return
def AddEdge(self,edge,head=None):
# Poor mans function overload
if head == None and len(edge) == 2:
t = edge[0]
h = edge[1]
else:
t = edge
h = head
try:
SubGraph.AddEdge(self,t,h)
self.Animator.SetEdgeColor(t,h,self.Color)
self.Animator.DefaultInfo()
except NoSuchVertexError, NoSuchEdgeError:
return
def DeleteEdge(self,edge,head=None):
if head == None and len(edge) == 2:
t = edge[0]
h = edge[1]
else:
t = edge
h = head
try:
SubGraph.DeleteEdge(self,t,h)
self.Animator.SetEdgeColor(t,h,"black")
except NoSuchVertexError, NoSuchEdgeError:
return
def Clear(self, color="grey"):
""" Delete all vertices and edges from the animated subgraph.
and color them with 'color' (grey is default) """
# GraphDisplay functions save several update()'s
self.Animator.SetAllVerticesColor(color,self)
self.Animator.SetAllEdgesColor(color,self)
self.vertices = []
self.adjLists = {}
self.invAdjLists = {} # Inverse Adjazenzlisten
self.size = 0
self.totalWeight = 0
def AddEdgeByVertices(self,tail,head):
try:
SubGraph.AddEdge(self,tail,head)
self.Animator.SetEdgeColor(tail,head,self.Color)
self.Animator.DefaultInfo()
except NoSuchVertexError, NoSuchEdgeError:
return
class AnimatedPredecessor(VertexLabeling):
""" Animates a predecessor array by
- coloring edges (pred[v],v) 'red'
- coloring edges (pred[v],v) 'grey' if the value of
pred[v] is changed """
def __init__(self, theAnimator, leaveColors = None, predColor='red'):
VertexLabeling.__init__(self)
self.Animator = theAnimator
self.leaveColors = leaveColors
self.predColor = predColor
def __setitem__(self, v, val):
try:
oldVal = VertexLabeling.__getitem__(self, v)
if oldVal != None:
if self.leaveColors == None or not (self.Animator.GetEdgeColor(oldVal,v) in self.leaveColors):
self.Animator.SetEdgeColor(oldVal,v,"grey")
except:
pass
if val != None:
try:
if self.leaveColors == None or not (self.Animator.GetEdgeColor(val,v) in self.leaveColors):
self.Animator.SetEdgeColor(val,v,self.predColor)
except:
pass
VertexLabeling.__setitem__(self, v, val)
def SetPredColor(self, color):
""" NOTE: This does not recolor assigned (pred[v],v) edges """
self.predColor = color
def AppendLeaveColor(self,color):
if self.leaveColors == None:
self.leaveColors = [color]
else:
self.leaveColors.append(color)
class ComponentMaker:
""" Subsequent calls of method NewComponent() will return differently
colored subgraphs of G """
def __init__(self,g,a):
self.G = g
self.A = a
self.colors = ['#FF0000','#00FF00','#0000FF',
'#009999','#990099','#999900',
'#996666','#669966','#666699',
'#0066CC','#6600CC','#66CC00',
'#00CC66','#CC0066','#CC6600']
self.lastColor = 0
def NewComponent(self):
comp = AnimatedSubGraph(self.G, self.A, self.colors[self.lastColor])
self.lastColor = self.lastColor + 1
if self.lastColor == len(self.colors):
self.lastColor = 0
return comp
def LastComponentColor(self):
if self.lastColor > 0:
return self.colors[self.lastColor -1]
return None
################################################################################
#
# Functions
#
################################################################################
def showPathByPredecessorArray(source,sink,pred,A,color="red"):
""" Visualizes a path from source to sink in a graph G
displayed in A. The path is specified in terms of the
predecessor array pred and will be colored with color
(default is 'red') """
v = sink
seen = [v] # avoid getting stuck in cycles
while (pred[v] != None) and (pred[v] != v):
A.SetVertexColor(v,color)
A.SetEdgeColor(pred[v],v,color)
v = pred[v]
if v in seen:
return
else:
seen.append(v)
A.SetVertexColor(v,color)
################################################################################
#
# Wrapper
#
################################################################################
class FlowWrapper:
""" This class visualizes the flow in a directed graph G
with animator GA and it's residual network R with
animator RA.
flow = FlowWrapper(G,A,R,RA,G.edgeWeights[0],R.edgeWeights[0])
or
flow = FlowWrapper(G,A,R,RA,G.edgeWeights[0],R.edgeWeights[0],G.vertexWeights[0])
"""
def __init__(self, G, GA, R, RA, flow, res, excess=None):
self.zeroEdgeColor = "black"
self.G = G
self.GA = GA
self.R = R
self.RA = RA
self.flow = flow
self.cap = copy.deepcopy(res)
self.res = res
self.excess = excess
if self.excess == None: ## if no startup excess set all to zero
self.excess = {}
for v in self.G.vertices:
self.excess[v] = 0
for e in self.G.Edges():
self.flow[e] = 0
def __setitem__(self, e, val):
if (self.excess[e[0]] != gInfinity) and (self.excess[e[0]] != -gInfinity):
self.excess[e[0]] = self.excess[e[0]] + self.flow[e] - val
if (self.excess[e[1]] != gInfinity) and (self.excess[e[1]] != -gInfinity):
self.excess[e[1]] = self.excess[e[1]] - self.flow[e] + val
if self.excess[e[0]] > 0:
self.RA.SetVertexColor(e[0],"green")
elif self.excess[e[0]] < 0:
self.RA.SetVertexColor(e[0],"red")
else:
self.RA.SetVertexColor(e[0],"gray")
if self.excess[e[1]] > 0:
self.RA.SetVertexColor(e[1],"green")
elif self.excess[e[1]] < 0:
self.RA.SetVertexColor(e[1],"red")
else:
self.RA.SetVertexColor(e[1],"gray")
self.flow[e] = val
if val == self.cap[e]:
self.GA.SetEdgeColor(e[0],e[1],"blue")
self.GA.SetEdgeAnnotation(e[0],e[1],"%d/%d" % (val,self.cap[e]),"black")
try:
self.RA.DeleteEdge(e[0],e[1])
except:
None
if not self.R.QEdge(e[1],e[0]):
self.RA.AddEdge(e[1],e[0])
elif val == 0:
self.GA.SetEdgeColor(e[0],e[1],self.zeroEdgeColor)
self.GA.SetEdgeAnnotation(e[0],e[1],"%d/%d" % (val, self.cap[e]),"gray")
try:
self.RA.DeleteEdge(e[1],e[0])
except:
None
if not self.R.QEdge(e[0],e[1]):
self.RA.AddEdge(e[0],e[1])
else:
self.GA.SetEdgeColor(e[0],e[1],"#9999FF")
self.GA.SetEdgeAnnotation(e[0],e[1],"%d/%d" % (val,self.cap[e]),"black")
if not self.R.QEdge(e[1],e[0]):
self.RA.AddEdge(e[1],e[0])
if not self.R.QEdge(e[0],e[1]):
self.RA.AddEdge(e[0],e[1])
if self.G.QEdge(e[0],e[1]):
self.res[(e[1],e[0])] = val
self.res[(e[0],e[1])] = self.cap[(e[0],e[1])] - val
else:
self.res[(e[0],e[1])] = val
self.res[(e[1],e[0])] = self.cap[(e[1],e[0])] - val
return
def __getitem__(self, e):
return self.flow[e]
class ReducedCostsWrapper:
""" Visualizes the reduced costs of the edge
>0 green
=0 grey
<0 red
"""
def __init__(self, A, cost, pot):
self.cost = cost
self.pot = pot
self.A = A
def __setitem__(self, e, val):
self.cost[e] = val
rc = self.cost[e] - self.pot[e[0]] + self.pot[e[1]]
try:
if rc > 0:
self.A.SetEdgeColor(e[0],e[1],"green")
elif rc == 0:
self.A.SetEdgeColor(e[0],e[1],"grey")
else:
self.A.SetEdgeColor(e[0],e[1],"red")
except:
None
def __getitem__(self, e):
return self.cost[e]
et equal to a copy of vertexSet """
self.vertices = vertexSet[:]
def Remove(self, v):
self.Animator.SetVertexColor(v,self.color)
self.vertices.remove(v)
def AddGato/BFS.alg 0100644 0017676 0017534 00000000336 10014153347 0014117 0 ustar 00schliep catbox 0000305 0050066 for v in Vertices:
visited[v] = 0
root = PickVertex()
visited[root] = 1
Q.Append(root)
while Q.IsNotEmpty():
v = Q.Top()
for w in Neighborhood(v):
if visited[w] == 0:
visited[w] = 1
Q.Append(w)
Gato/BFS.pro 0100644 0017676 0017534 00000004262 10014153347 0014156 0 ustar 00schliep catbox 0000305 0050066 ################################################################################
#
# This is part of Gato (Graph Algorithm Toolbox)
# You can find more information at
# http://gato.sf.net
#
# file: BFS.pro
# author: Alexander Schliep (schliep@molgen.mpg.de)
#
# Copyright (C) 1998-2003, Alexander Schliep, Winfried Hochstaettler and
# ZAIK/ZPR, Universitaet zu Koeln
#
# Contact: schliep@molgen.mpg.de, wh@zpr.uni-koeln.de
#
# Information: http://gato.sf.net
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Library General Public
# License as published by the Free Software Foundation; either
# version 2 of the License, or (at your option) any later version.
#
# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Library General Public License for more details.
#
# You should have received a copy of the GNU Library General Public
# License along with this library; if not, write to the Free
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#
#
#
# This file has version _FILE_REVISION_ from _FILE_DATE_
#
#
################################################################################
# Options ----------------------------------------------------------------------
breakpoints = [9]
interactive = [4]
graphDisplays = 1
about = """
Breadth-First-Search
This algorithm traverses a graph in breadth-first
order.
"""
#--------------------------------------------------------------------------------
#self.NeededProperties({'pMist':1})
pickCallback = lambda v, a=A: A.SetVertexAnnotation(v,"source")
PickVertex = lambda f=pickCallback: self.PickVertex(1,None,f)
Neighborhood = lambda v,a=A,g=G: AnimatedNeighborhood(a,g,v)
Vertices = G.vertices
visited = AnimatedVertexLabeling(A)
Q = AnimatedVertexQueue(A)
# End-of BFS.pro
Gato/DFS.alg 0100644 0017676 0017534 00000000333 10014153347 0014116 0 ustar 00schliep catbox 0000305 0050066 for v in Vertices:
visited[v] = 0
root = PickVertex()
visited[root] = 1
S.Push(root)
while S.IsNotEmpty():
v = S.Pop()
for w in Neighborhood(v):
if visited[w] == 0:
visited[w] = 1
S.Push(w)
Gato/DFS.pro 0100644 0017676 0017534 00000004211 10014153350 0014144 0 ustar 00schliep catbox 0000305 0050066 ################################################################################
#
# This is part of Gato (Graph Algorithm Toolbox)
# You can find more information at
# http://gato.sf.net
#
# file: DFS.pro
# author: Alexander Schliep (schliep@molgen.mpg.de)
#
# Copyright (C) 1998-2003, Alexander Schliep, Winfried Hochstaettler and
# ZAIK/ZPR, Universitaet zu Koeln
#
# Contact: schliep@molgen.mpg.de, wh@zpr.uni-koeln.de
#
# Information: http://gato.sf.net
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Library General Public
# License as published by the Free Software Foundation; either
# version 2 of the License, or (at your option) any later version.
#
# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Library General Public License for more details.
#
# You should have received a copy of the GNU Library General Public
# License along with this library; if not, write to the Free
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#
#
#
# This file has version _FILE_REVISION_ from _FILE_DATE_
#
#
################################################################################
# Options ----------------------------------------------------------------------
breakpoints = []
interactive = []
graphDisplays = 1
about = """
Depth-First-Search
This algorithm traverses a graph in depth-first
order.
"""
#--------------------------------------------------------------------------------
pickCallback = lambda v, a=A: A.SetVertexAnnotation(v,"source")
PickVertex = lambda f=pickCallback: self.PickVertex(1,None,f)
Neighborhood = lambda v,a=A,g=G: AnimatedNeighborhood(a,g,v)
Vertices = G.vertices
visited = AnimatedVertexLabeling(A)
S = AnimatedVertexStack(A)
# End-of DFS.pro
Gato/DataStructures.py 0100644 0017676 0017534 00000025361 10014153350 0016346 0 ustar 00schliep catbox 0000305 0050066 ################################################################################
#
# This file is part of Gato (Graph Animation Toolbox)
# You can find more information at
# http://gato.sf.net
#
# file: DataStructures.py
# author: Alexander Schliep (schliep@molgen.mpg.de)
#
# Copyright (C) 1998-2003, Alexander Schliep, Winfried Hochstaettler and
# ZAIK/ZPR, Universitaet zu Koeln
#
# Contact: schliep@molgen.mpg.de, wh@zpr.uni-koeln.de
#
# Information: http://gato.sf.net
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Library General Public
# License as published by the Free Software Foundation; either
# version 2 of the License, or (at your option) any later version.
#
# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Library General Public License for more details.
#
# You should have received a copy of the GNU Library General Public
# License along with this library; if not, write to the Free
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#
#
#
# This file is version $Revision: 1.15 $
# from $Date: 2003/11/12 13:39:48 $
# last change by $Author: schliep $.
#
################################################################################
from GatoGlobals import *
from __future__ import generators #Needed for PQImplementation
################################################################################
#
# Embedding
#
################################################################################
class Point2D:
""" Simple Wrapper class for a point in 2D-space. Used for Graph
Embeddings. Use: Point2D([x,y]) or Point2D(x,y) """
def __init__(self, x = None, y = None):
if y == None:
if x == None:
self.x = 0
self.y = 0
else:
self.x = x[0]
self.y = x[1]
self.x = x
self.y = y
################################################################################
#
# Vertex Labeling
#
################################################################################
class VertexLabeling:
""" Simple Wrapper class for any mapping of vertices to values.
E.g.,
- strings (for labels)
- Point2D (for embeddings) """
def __init__(self):
self.label = {}
def __setitem__(self, v, val):
self.label[v] = val
def __getitem__(self, v):
return self.label[v]
def keys(self):
return self.label.keys()
def QDefined(self,v):
return v in self.label.keys()
class VertexWeight(VertexLabeling):
def __init__(self, theGraph, initialWeight = None):
VertexLabeling.__init__(self)
self.G = theGraph
self.integer = 0
if initialWeight is not None:
self.SetAll(initialWeight)
def QInteger(self):
""" Returns 1 if all weights are integers, 0 else """
return self.integer
def Integerize(self):
if not self.integer:
for v in self.label.keys():
self.label[v] = int(round(self.label[v]))
self.integer = 1
def SetAll(self, initialWeight):
for v in self.G.vertices:
self.label[v] = initialWeight
################################################################################
#
# Edge Labeling
#
################################################################################
class EdgeLabeling:
""" Simple wrapper class for any mapping of edges to values.
E.g.,
- draw edges (for GraphDisplay)
- weights (for embeddings)
Use EdgeLabeling[(u,v)] for access """
def __init__(self):
self.label = {}
def __setitem__(self, e, val): # Use with (tail,head)
self.label[e] = val
def __getitem__(self, e):
return self.label[e]
def QDefined(self,e):
return e in self.label.keys()
class EdgeWeight(EdgeLabeling):
""" Simple class for storing edge weights.
Use EdgeWeight[(u,v)] for access, undirected graphs are
handled properly. """
def __init__(self, theGraph, initialWeight = None):
EdgeLabeling.__init__(self)
self.G = theGraph
self.integer = 0
if initialWeight is not None:
self.SetAll(initialWeight)
def __setitem__(self, e, val): # Use with (tail,head)
if self.G.QDirected():
self.label[e] = val
else:
try:
tmp = self.label[(e[1], e[0])]
self.label[(e[1], e[0])] = val
except KeyError:
self.label[e] = val
def __getitem__(self, e):
if self.G.QDirected():
return self.label[e]
else:
try:
return self.label[(e[1], e[0])]
except KeyError:
return self.label[e]
def QInteger(self):
""" Returns 1 if all weights are integers, 0 else """
return self.integer
def Integerize(self):
if not self.integer:
for e in self.label.keys():
self.label[e] = int(round(self.label[e]))
self.integer = 1
def SetAll(self, initialWeight):
for e in self.G.Edges():
self.label[e] = initialWeight
################################################################################
#
# Queue
#
################################################################################
class Queue:
""" Simple Queue class implemented as a Python list:
XXX check whether replaceble by library queue"""
def __init__(self, elems=None):
if elems == None:
self.contents = []
else:
self.contents = elems[:]
def Append(self,v):
self.contents.append(v)
def Top(self):
v = self.contents[0]
self.contents = self.contents[1:]
return v
def Clear(self):
self.contents = []
def IsEmpty(self):
return (len(self.contents) == 0)
def IsNotEmpty(self):
return (len(self.contents) > 0)
def Contains(self,v):
return v in self.contents
################################################################################
#
# PriorityQueue
#
################################################################################
class PQImplementation(dict):
""" Heap based implementation """
def __init__(self):
'''Initialize priorityDictionary by creating binary heap of
pairs (value,key). Note that changing or removing a dict
entry will not remove the old pair from the heap until it is
found by smallest() or until the heap is rebuilt.'''
self.__heap = []
dict.__init__(self)
def smallest(self):
'''Find smallest item after removing deleted items from heap.'''
if len(self) == 0:
raise IndexError, "smallest of empty priorityDictionary"
heap = self.__heap
while heap[0][1] not in self or self[heap[0][1]] != heap[0][0]:
lastItem = heap.pop()
insertionPoint = 0
while 1:
smallChild = 2*insertionPoint+1
if smallChild+1 < len(heap) and \
heap[smallChild] > heap[smallChild+1]:
smallChild += 1
if smallChild >= len(heap) or lastItem <= heap[smallChild]:
heap[insertionPoint] = lastItem
break
heap[insertionPoint] = heap[smallChild]
insertionPoint = smallChild
return heap[0][1]
def __iter__(self):
'''Create destructive sorted iterator of priorityDictionary.'''
def iterfn():
while len(self) > 0:
x = self.smallest()
yield x
del self[x]
return iterfn()
def deleteMin(self):
x = self.smallest()
del self[x]
return x
def __setitem__(self,key,val):
'''Change value stored in dictionary and add corresponding
pair to heap. Rebuilds the heap if the number of deleted
items grows too large, to avoid memory leakage.'''
dict.__setitem__(self,key,val)
heap = self.__heap
if len(heap) > 2 * len(self):
self.__heap = [(v,k) for k,v in self.iteritems()]
self.__heap.sort() # builtin sort likely faster than O(n) heapify
else:
newPair = (val,key)
insertionPoint = len(heap)
heap.append(None)
while insertionPoint > 0 and \
newPair < heap[(insertionPoint-1)//2]:
heap[insertionPoint] = heap[(insertionPoint-1)//2]
insertionPoint = (insertionPoint-1)//2
heap[insertionPoint] = newPair
def setdefault(self,key,val):
'''Reimplement setdefault to call our customized __setitem__.'''
if key not in self:
self[key] = val
return self[key]
def update(self, other):
for key in other.keys():
self[key] = other[key]
class PriorityQueue:
""" A simple priority queue giving minimal valued items first.
Interface only ... """
def __init__(self):
self.pq = PQImplementation()
def Insert(self,value,sortKey):
self.pq[value] = sortKey
def DeleteMin(self):
""" Return and delete minimal value with minimal sortKey from queue. """
return self.pq.deleteMin()
def DecreaseKey(self,value,newSortKey):
if self.pq.has_key(value):
self.pq[value] = newSortKey
else:
print "PriorityQueue: DecreaseKey of non-existing key"
raise KeyError, "PriorityQueue: DecreaseKey of non-existing key"
def Clear(self):
del self.pq
self.pq = PQImplementation()
def IsEmpty(self):
return (len(self.pq) == 0)
def IsNotEmpty(self):
return (len(self.pq) > 0)
################################################################################
#
# Stack
#
################################################################################
class Stack:
""" Simple Stack class implemented as a Python list """
def __init__(self):
self.contents = []
def Push(self,v):
self.contents.append(v)
def Pop(self):
v = self.contents[-1]
self.contents = self.contents[:-1]
return v
def Clear(self):
self.contents = []
def IsEmpty(self):
return (len(self.contents) == 0)
def IsNotEmpty(self):
return (len(self.contents) > 0)
def Contains(self,v):
return v in self.contents
################################################################################
#
# Set
#
################################################################################
class Set:
def __init__(self):
self.members = []
return
def __getitem__(self,key):
return self.members[key]
def Add(self, e):
self.members.append(e)
return
def Delete(self, e):
try:
self.members.remove(e)
except:
None
return
def IsNotEmpty(self):
return len(self.members) > 0
def Contains(self,e):
return e in self.members
Gato/EditObjectAttributesDialog.py 0100644 0017676 0017534 00000037430 10014153350 0020574 0 ustar 00schliep catbox 0000305 0050066 #!/usr/bin/env python
################################################################################
#
# This file is part of Gato (Graph Algorithm Toolbox)
# You can find more information at
# http://gato.sf.net
#
# file: EditObjectAttributesDialog.py
# author: Alexander Schliep (schliep@molgen.mpg.de)
#
# Copyright (C) 1998-2003, Alexander Schliep, Winfried Hochstaettler and
# ZAIK/ZPR, Universitaet zu Koeln
#
# Contact: schliep@molgen.mpg.de, wh@zpr.uni-koeln.de
#
# Information: http://gato.sf.net
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Library General Public
# License as published by the Free Software Foundation; either
# version 2 of the License, or (at your option) any later version.
#
# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Library General Public License for more details.
#
# You should have received a copy of the GNU Library General Public
# License along with this library; if not, write to the Free
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#
#
#
# This file is version $Revision: 1.13 $
# from $Date: 2003/02/12 14:24:57 $
# last change by $Author: schliep $.
#
################################################################################
from Tkinter import *
from ScrolledText import *
import tkSimpleDialog
import tkMessageBox
from tkColorChooser import askcolor
import copy
import sys
import os
import types
def typed_assign(var, val):
result = type(var)(val)
result.__dict__ = copy.copy(var.__dict__)
return result
#-------------------------------------------------------------------------------
class TkStringEntry:
"""Tk entry field for editing strings"""
def __init__(self, master, width):
self.entryWidget = Entry(master, width=width, exportselection=FALSE)
def tkWidget(self):
return self.entryWidget
def get(self):
return self.entryWidget.get()
def set(self, value):
self.entryWidget.delete(0,END)
self.entryWidget.insert(0,"%s" % value)
def select(self):
self.entryWidget.selection_range(0,"end")
self.entryWidget.focus_set()
class TkIntEntry(TkStringEntry):
"""Tk entry field for editing one integer"""
def get(self):
return int(self.entryWidget.get())
class TkFloatEntry(TkStringEntry):
"""Tk entry field for editing one float"""
def get(self):
return float(self.entryWidget.get())
class TkDefaultMixin:
"""Mixin for TkStringEntry, TkIntEntry, TkFloatEntry, ... to deal with
values which have an externally defined default value. Combination
of 'use default' checkbox and corresponding entry field """
def __init__(self, master, useDefault, defaultValue):
self.frame = Frame(master, relief=FLAT)
self.useDefault = IntVar()
self.useDefault.set(useDefault)
self.defaultValue = defaultValue
useDefaultButton = Checkbutton(self.frame, text="Use default",
variable=self.useDefault,
command=self.toggleDefault)
useDefaultButton.grid(row=0, column=0, padx=4, pady=3, sticky=W)
def finish(self):
self.entryWidget.grid(row=0, column=1, padx=4, pady=3, sticky=W)
self.switchDefault(self.useDefault.get())
def UseDefault(self):
return self.useDefault.get()
def switchDefault(self, value):
if value == 0:
self.entryWidget['state'] = NORMAL
self.entryWidget.delete(0,END)
self.set(self.defaultValue)
else:
self.entryWidget.delete(0,END)
self.entryWidget['state'] = DISABLED
def toggleDefault(self):
self.switchDefault(self.useDefault.get())
class TkDefaultStringEntry(TkStringEntry, TkDefaultMixin):
def __init__(self, master, width, useDefault, defaultValue):
TkDefaultMixin.__init__(self, master, useDefault, defaultValue)
TkStringEntry.__init__(self, self.frame, width)
self.finish()
def tkWidget(self): # To avoid ambiguity
return self.frame
class TkDefaultIntEntry(TkIntEntry, TkDefaultMixin):
def __init__(self, master, width, useDefault, defaultValue):
TkDefaultMixin.__init__(self, master, useDefault, defaultValue)
TkIntEntry.__init__(self, self.frame, width)
self.finish()
def tkWidget(self): # To avoid ambiguity
return self.frame
def get(self):
if self.UseDefault():
return self.defaultValue
else:
return TkIntEntry.get(self)
class TkDefaultFloatEntry(TkFloatEntry, TkDefaultMixin):
def __init__(self, master, width, useDefault, defaultValue):
TkDefaultMixin.__init__(self, master, useDefault, defaultValue)
TkFloatEntry.__init__(self, self.frame, width)
self.finish()
def tkWidget(self): # To avoid ambiguity
return self.frame
def get(self):
if self.UseDefault():
return self.defaultValue
else:
return TkFloatEntry.get(self)
class TkPopupSelector:
def __init__(self, master, value2pop, pop2value, width):
self.value2pop = value2pop
self.pop2value = pop2value
self.popupvalue = StringVar()
self.popupvalue.set(self.pop2value.keys()[0]) # XXX first value as default
# XXX Uuughhh
keys = self.value2pop.keys()
keys.sort()
pops = map(lambda x: value2pop[x], keys)
#log.debug("pops = %s" % pops)
args = (master, self.popupvalue) + tuple(pops)
self.tkwidget = apply(OptionMenu, args)
self.tkwidget.config(height=1, width=width)
def tkWidget(self):
return self.tkwidget
def get(self):
return self.pop2value[self.popupvalue.get()]
def set(self, value):
try:
self.popupvalue.set(self.value2pop[value])
except:
self.popupvalue.set(self.pop2value.keys()[0]) # XXX first value as default
def select(self):
# Cant choose invalid value with popup
pass
class TkStringPopupSelector:
def __init__(self, master, strings):
self.strings = strings
self.popupvalue = StringVar()
if len(self.strings) > 0:
self.popupvalue.set(self.strings[0]) # XXX first value as default
width = max(map(len, self.strings))
args = (master, self.popupvalue) + tuple(self.strings)
self.tkwidget = apply(OptionMenu, args)
self.tkwidget.config(height=1, width=width)
def tkWidget(self):
return self.tkwidget
def get(self):
return self.popupvalue.get()
def set(self, value):
try:
self.popupvalue.set(value)
except:
self.popupvalue.set(self.strings[0]) # XXX first value as default
def select(self):
# Cant choose invalid value with popup
pass
class TkColorSelector:
def __init__(self, master, color='black'):
#self.tkwidget = Button(master, width=8, command=self.editColor)
self.tkwidget = Frame(master, height=18, width=60, relief=RIDGE, borderwidth=1)
self.tkwidget.bind("", self.editColor)
self.set(color)
def editColor(self, event):
color = askcolor(self.color)[1]
if color is not None:
self.set(color)
def tkWidget(self):
return self.tkwidget
def get(self):
return self.color
def set(self, value):
self.color = value
self.tkwidget.config(bg=self.color)
def select(self):
# Cant choose invalid value with popup
pass
class EditObjectAttributesDialog(tkSimpleDialog.Dialog):
""" Creates an editable (pseudo-)inspector for a selected set of
attributes of a given object
- master : tk master widget
- object : the object, whose attributes we want to edit
- attr_names : a list of attr_names
By making use of Python 2.2's capability of subclassing built-in
types such as ints, information about editing etc. is conveyed.
An attr must have:
- validate(value) method [return 1, if value is a valid new value for attr]
The class of an attr can have the following mix-ins:
- Popubable
- WithDefault
"""
def __init__(self, master, object, attr_names):
self.object = object
self.attr_names = attr_names
self.edit = {}
tkSimpleDialog.Dialog.__init__(self, master, "Edit: %s" % self.object.desc)
def editWidget(self, master, object, attr_name):
""" Create a widget capable of editing attr and insert attr's current value"""
attr = object.__dict__[attr_name]
attr_type = type(attr)
widget = None
default = isinstance(attr, WithDefault) # has a WithDefault mixin
if isinstance(attr, Popupable):
widget = TkPopupSelector(master, attr.val2pop, attr.pop2val, attr.width)
elif isinstance(attr, str):
if default:
widget = TkDefaultStringEntry(master, max(32, len(attr)), attr.useDefault, attr)
else:
widget = TkStringEntry(master, max(32, len(attr)))
elif isinstance(attr, int):
if default:
widget = TkDefaultIntEntry(master, 6, attr.useDefault, attr)
else:
widget = TkIntEntry(master, 6)
elif isinstance(attr, float):
if default:
widget = TkDefaultFloatEntry(master, 8, attr.useDefault, attr)
else:
widget = TkFloatEntry(master, 8)
widget.set(attr)
return widget
def body(self, master):
self.resizable(0,0)
# Header Zeile
label = Label(master, text="Name", anchor=E)
label.grid(row=0, column=0, padx=4, pady=3, sticky=E)
label = Label(master, text="Value", anchor=W)
label.grid(row=0, column=1, padx=4, pady=3, sticky=W)
cur_row = 1
for attr in self.attr_names:
label = Label(master, text="%s" % attr, anchor=E)
label.grid(row=cur_row, column=0, padx=4, pady=3, sticky=E)
self.edit[attr] = self.editWidget(master, self.object, attr)
if self.edit[attr] != None:
self.edit[attr].tkWidget().grid(row=cur_row, column=1, padx=2, pady=1, sticky=W)
cur_row = cur_row + 1
def validate(self):
for attr_name in self.edit.keys():
try:
# In python 2.2 we can subclass attributes and add a validate method
# to attributes
value = self.edit[attr_name].get()
if self.object.__dict__[attr_name].validate(value) == 0:
raise ValueError
except ValueError:
msg = "Please enter a valid value for %s" % attr_name
tkMessageBox.showwarning("Invalid Value", msg, parent=self)
self.edit[attr_name].select()
return 0
# Everything is valid => set values
for attr_name in self.edit.keys():
self.object.__dict__[attr_name] = typed_assign(self.object.__dict__[attr_name], self.edit[attr_name].get())
if isinstance(self.object.__dict__[attr_name], WithDefault):
self.object.__dict__[attr_name].useDefault = self.edit[attr_name].useDefault.get()
return 1
#-------------------------------------------------------------------------------
class WithDefault:
"""Mix-in for variables which have a default value"""
def setDefault(self, useDefault, defaultValue):
self.useDefault = useDefault
self.defaultValue = defaultValue
def validate(self, value):
## if self.useDefault:
## return 1
## else:
## return 1 # XXX How can I call a method of the class I am mixed too
return 1
class Popupable:
"""Mix-in for variables which can be edited via a pop-up menu
- val2pop : dict mapping value to string for pop up menu
- pop2val: dict mapping pop up menu string to value
- width: maximal string length in pop up
"""
def setPopup(self, val2pop, pop2val = None, width = None):
self.val2pop = val2pop
self.pop2val = None
self.width = None
if pop2val == None:
self.pop2val = {} # Private copy
self.width = 0
for val in val2pop.keys():
pop = val2pop[val]
self.width = max(len(pop), self.width)
self.pop2val[pop] = val
else:
self.pop2val = pop2val
self.width = width
def validate(self, value):
return 1
##class PopupableStr(str):
## """Class for variables which can be edited via a pop-up menu
## - values: array of values
## - width: maximal string length in pop up
## """
## def setPopup(self, values, width = None):
## self.values = values
## self.width = width
## if width == None:
## self.width = 0
## for s in values:
## self.width = max(len(s), self.width)
## def validate(self, value):
## return 1
class AlwaysValidate:
"""Mix-in for variables which always are valid"""
def validate(self, value):
return 1
#-------------------------------------------------------------------------------
class ValidatingInt(int, AlwaysValidate):
"""Editable replacement for ints"""
pass
class ValidatingFloat(float, AlwaysValidate):
"""Editable replacement for floats"""
pass
class ValidatingString(str, AlwaysValidate):
"""Editable replacement for strings"""
pass
class PopupableInt(int, Popupable):
"""A replacement for ints editable via a pop-up"""
pass
class Probability(float):
"""An editable float taking values from [0,1]"""
def validate(self, value):
if 0.0 <= value and value <= 1.0:
return 1
else:
return 0
class DefaultedInt(int, WithDefault):
"""An editable int with a default value"""
pass
class DefaultedFloat(float, WithDefault):
"""An editable float with a default value"""
pass
class DefaultedString(str, WithDefault):
"""An editable strinf with a default value"""
pass
#======================================================================
#
# Demo:
#
class TkTestFrame(Frame):
def __init__(self, parent=None):
Frame.__init__(self,parent)
Pack.config(self)
self.createWidgets()
self.desc = ValidatingString("The TkTestFrame")
self.x = DefaultedInt(1)
self.x.setDefault(1, 122)
self.y = ValidatingFloat(2.33)
self.choose = PopupableInt(3)
self.pop2val = {"aaa":1, "xxx":2, "sss":3}
self.val2pop = {1:"aaa", 2:"xxx", 3:"sss"}
self.choose.setPopup(self.val2pop, self.pop2val, 5)
def createWidgets(self):
self.QUIT = Button(self, text='QUIT', foreground='red',
command=self.quit)
self.QUIT.pack(side=LEFT)
self.About = Button(self, text='Preferences', foreground='red',
command=self.About)
self.About.pack(side=LEFT)
def About(self):
aboutBox = EditObjectAttributesDialog(self.master, self, ['desc', 'x', 'y', 'choose'])
del self.pop2val["aaa"]
del self.val2pop[1]
aboutBox = EditObjectAttributesDialog(self.master, self, ['desc', 'x', 'y', 'choose'])
if __name__ == '__main__':
app = TkTestFrame()
app.mainloop()
ys.sort()
pops = map(lambda x: value2pop[x], keys)
#log.debug("pops = %s" % pops)
args = (master, self.popupvalue) + tuple(pops)
self.tkwidget = apply(OptionMenu, args)
self.tkwidGato/Embedder.py 0100644 0017676 0017534 00000040042 10014153350 0015071 0 ustar 00schliep catbox 0000305 0050066 ################################################################################
#
# This file is part of Gato (Graph Animation Toolbox)
# You can find more information at
# http://gato.sf.net
#
# file: Embedder.py
# author: Ramazan Buzdemir
#
# Copyright (C) 1998-2003, Alexander Schliep, Winfried Hochstaettler and
# ZAIK/ZPR, Universitaet zu Koeln
#
# Contact: schliep@molgen.mpg.de, wh@zpr.uni-koeln.de
#
# Information: http://gato.sf.net
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Library General Public
# License as published by the Free Software Foundation; either
# version 2 of the License, or (at your option) any later version.
#
# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Library General Public License for more details.
#
# You should have received a copy of the GNU Library General Public
# License along with this library; if not, write to the Free
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#
#
#
# This file is version $Revision: 1.19 $
# from $Date: 2003/03/07 15:11:28 $
# last change by $Author: buzdemir $.
#
################################################################################
from GatoGlobals import *
class Embedder:
""" This class provides an abstract Embedder as
a base for actual Embedder implementations """
def Name(self):
""" Return a short descriptive name for the embedder e.g. usable as
a menu item """
return "none"
def Embed(self, theGraphEditor):
""" Compute the Embedding. Changed display through theGraphEditor.
Return value != none designates error/warning message """
return none
def RedrawGraph(theGraphEditor):
theGraphEditor.SetGraphMenuGrid(0)
for v in theGraphEditor.G.vertices:
theGraphEditor.MoveVertex(v, theGraphEditor.G.xCoord[v],
theGraphEditor.G.yCoord[v], 1)
#----------------------------------------------------------------------
import whrandom
def RandomCoords(G):
G.xCoord={}
G.yCoord={}
for v in G.vertices:
G.xCoord[v]=whrandom.randint(10,990)
G.yCoord[v]=whrandom.randint(10,990)
return 1
class RandomEmbedder(Embedder):
def Name(self):
return "Randomize Layout"
def Embed(self, theGraphEditor):
if theGraphEditor.G.Order()==0:
return
theGraphEditor.config(cursor="watch")
theGraphEditor.update()
if RandomCoords(theGraphEditor.G):
RedrawGraph(theGraphEditor)
theGraphEditor.config(cursor="")
#----------------------------------------------------------------------
from math import pi, sin, cos
def CircularCoords(G):
G.xCoord={}
G.yCoord={}
distance = 2*pi/G.Order()
degree = 0
xMiddle=500; yMiddle=500; radius=450
for v in G.vertices:
G.xCoord[v]=radius*cos(degree)+xMiddle
G.yCoord[v]=radius*sin(degree)+yMiddle
degree=degree+distance
return 1
class CircularEmbedder(Embedder):
def Name(self):
return "Circular Layout"
def Embed(self, theGraphEditor):
if theGraphEditor.G.Order()==0:
return
theGraphEditor.config(cursor="watch")
theGraphEditor.update()
if CircularCoords(theGraphEditor.G):
RedrawGraph(theGraphEditor)
theGraphEditor.config(cursor="")
#----------------------------------------------------------------------
from PlanarEmbedding import *
class FPP_PlanarEmbedder(Embedder):
def Name(self):
return "Planar Layout (FPP)"
def Embed(self, theGraphEditor):
theGraphEditor.config(cursor="watch")
theGraphEditor.update()
if theGraphEditor.G.Order()==0:
return
if FPP_PlanarCoords(theGraphEditor.G):
RedrawGraph(theGraphEditor)
theGraphEditor.config(cursor="")
class Schnyder_PlanarEmbedder(Embedder):
def Name(self):
return "Planar Layout (Schnyder)"
def Embed(self, theGraphEditor):
if theGraphEditor.G.Order()==0:
return
theGraphEditor.config(cursor="watch")
theGraphEditor.update()
if Schnyder_PlanarCoords(theGraphEditor.G):
RedrawGraph(theGraphEditor)
theGraphEditor.config(cursor="")
#----------------------------------------------------------------------
from Tkinter import *
import tkSimpleDialog
import string
from tkMessageBox import showwarning
from DataStructures import Stack
"""
def center(G):
# Floyd-Algorithm
INFTY=9999999
dist={}
for v in G.vertices:
for w in G.vertices:
if w in G.InOutNeighbors(v):
dist[v,w]=1
elif v==w:
dist[v,w]=0
else:
dist[v,w]=INFTY
for u in G.vertices:
for v in G.vertices:
for w in G.vertices:
if dist[v,u]+dist[u,w]max2:
max2=dist[u,v]
if max2height:
height = d[w]
nodes[height] = []
S.Push(w)
if isleaf:
number_of_leaves = number_of_leaves + 1
if orientation=="vertical":
leaves.insert(0,v)
else:
leaves.append(v)
# Test whether the graph is connected and
# acyclic.(=test whether the graph is a tree)
for v in G.vertices:
if visited[v]==0:
showwarning("Warning",
"Graph is not a tree,\n"
"not connected !!!")
return 0
ch_len = len(children[v])
if v!=root: ch_len = ch_len + 1
if ch_len=0:
for v in nodes[i]:
if children[v]!=[]:
Coord2[v] = 50 + d[v] * dist2
if len(children[v])==1:
Coord1[v] = Coord1[children[v][0]]
else:
Coord1[v] = ( Coord1[children[v][0]] +
(Coord1[children[v][-1]] -
Coord1[children[v][0]]) / 2)
i=i-1
if orientation=="vertical":
G.xCoord=Coord1
G.yCoord=Coord2
else:
G.xCoord=Coord2
G.yCoord=Coord1
return 1
class TreeEmbedder(Embedder):
def Name(self):
return "Tree Layout"
def Embed(self, theGraphEditor):
if theGraphEditor.G.Order()==0:
return
theGraphEditor.config(cursor="watch")
dial = TreeLayoutDialog(theGraphEditor)
if dial.result is None:
theGraphEditor.config(cursor="")
return
if TreeCoords(theGraphEditor.G, dial.result[0], dial.result[1]):
RedrawGraph(theGraphEditor)
theGraphEditor.config(cursor="")
#----------------------------------------------------------------------
from GraphUtil import BFS
class BFSLayoutDialog(tkSimpleDialog.Dialog):
def __init__(self, master):
self.G = master.G
tkSimpleDialog.Dialog.__init__(self, master, "BFS Layout")
def body(self, master):
self.resizable(0,0)
self.root=StringVar()
self.root.set(self.G.vertices[0])
label = Label(master, text="root :" , anchor=W)
label.grid(row=0, column=0, padx=0, pady=2, sticky="w")
entry=Entry(master, width=6, exportselection=FALSE,textvariable=self.root)
entry.selection_range(0,"end")
entry.focus_set()
entry.grid(row=0,column=1, padx=2, pady=2, sticky="w")
self.direction=StringVar()
self.direction.set("forward")
if self.G.QDirected():
radio=Radiobutton(master, text="forward", variable=self.direction,
value="forward")
radio.grid(row=0, column=2, padx=2, pady=2, sticky="w")
radio=Radiobutton(master, text="backward", variable=self.direction,
value="backward")
radio.grid(row=1, column=2, padx=2, pady=2, sticky="w")
def validate(self):
try:
if (string.atoi(self.root.get())<0 or
string.atoi(self.root.get()) not in self.G.vertices):
raise rootError
self.result=[]
self.result.append(string.atoi(self.root.get()))
self.result.append(self.direction.get())
return self.result
except:
showwarning("Warning",
"Invalid root !!!\n"
"Please try again !")
return 0
def BFSTreeCoords(G, root, direction):
BFSdistance = BFS(G,root,direction)[0]
maxDistance=0
maxBreadth=0
list = {}
for v in G.vertices:
list[BFSdistance[v]] = []
for v in G.vertices:
list[BFSdistance[v]].append(v)
maxDistance=len(list)
for d in list.values():
if len(d)>maxBreadth: maxBreadth=len(d)
if maxDistance > 1:
xDist=900/(maxDistance-1)
else:
xDist=0
if maxBreadth > 1:
yDist=900/(maxBreadth-1)
else:
yDist=0
Coord1=950
G.xCoord={}
G.yCoord={}
for d in list.values():
Coord2=500-(len(d)-1)*yDist/2
for v in d:
G.xCoord[v]=Coord1+whrandom.randint(-20,20)
G.yCoord[v]=Coord2
Coord2=Coord2+yDist
Coord1=Coord1-xDist
return 1
class BFSTreeEmbedder(Embedder):
def Name(self):
return "BFS-Tree Layout"
def Embed(self, theGraphEditor):
if theGraphEditor.G.Order()==0:
return
theGraphEditor.config(cursor="watch")
dial = BFSLayoutDialog(theGraphEditor)
if dial.result is None:
theGraphEditor.config(cursor="")
return
if BFSTreeCoords(theGraphEditor.G, dial.result[0], dial.result[1]):
RedrawGraph(theGraphEditor)
theGraphEditor.config(cursor="")
from math import *
from DataStructures import Queue
def RadialTreeBFS(G,root,direction='forward'):
""" Calculate BFS distances and predecessor without showing animations.
Also compute angles for a radial layout
If G is directed, direction does matter:
- 'forward' BFS will use outgoing edges
- 'backward' BFS will use incoming edges
It uses gInfinity (from GatoGlobals.py) as infinite distance.
returns (dist,pred) """
Q = Queue()
d = {}
pred = {}
angle = {}
childrenrange = {}
for v in G.vertices:
d[v] = gInfinity
d[root] = 0
pred[root] = None
angle[root] = 0
childrenrange[root] = (0, 2 * pi)
Q.Append(root)
while Q.IsNotEmpty():
v = Q.Top()
if G.QDirected() == 1 and direction == 'backward':
nbh = G.InNeighbors(v)
else:
nbh = G.Neighborhood(v)
# Compute size of unseen Nbh
unseen = 0
for w in nbh:
if d[w] == gInfinity:
unseen += 1
if unseen > 0:
range = childrenrange[v][1] - childrenrange[v][0]
delta = range / float(unseen)
delta2 = delta * 0.5
left = childrenrange[v][0] + delta2
for w in nbh:
if d[w] == gInfinity:
angle[w] = left + delta2
childrenrange[w] = (left,left+delta)
left += delta
d[w] = d[v] + 1
#print (v,w), "angle = ", angle[w]," range = ", childrenrange[w]
Q.Append(w)
return (d,pred,angle)
def RadialToXY(degree, r, offset):
return (r*sin(degree) + offset[0], r*cos(degree) + offset[1])
def BFSRadialTreeCoords(G, root, direction):
(BFSdistance,pred,angle) = RadialTreeBFS(G,root,direction)
maxdist = max(max(BFSdistance.values()),1)
G.xCoord={}
G.yCoord={}
offset = (500,500)
d = 450 / maxdist
for v in G.vertices:
try:
(G.xCoord[v], G.yCoord[v]) = RadialToXY(angle[v], BFSdistance[v] * d, offset)
except:
return 0
return 1
class BFSRadialTreeEmbedder(Embedder):
def Name(self):
return "BFS-Radial Tree Layout"
def Embed(self, theGraphEditor):
if theGraphEditor.G.Order()==0:
return
theGraphEditor.config(cursor="watch")
dial = BFSLayoutDialog(theGraphEditor)
if dial.result is None:
theGraphEditor.config(cursor="")
return
if BFSRadialTreeCoords(theGraphEditor.G, dial.result[0], dial.result[1]):
RedrawGraph(theGraphEditor)
theGraphEditor.config(cursor="")
#----------------------------------------------------------------------
""" Here instantiate all the embedders you want to make available to
a client. """
embedder = [RandomEmbedder(), CircularEmbedder(),
FPP_PlanarEmbedder(), Schnyder_PlanarEmbedder(),
TreeEmbedder(), BFSTreeEmbedder(), BFSRadialTreeEmbedder()]
er, text="vertical", variable=self.orientation,
value="vertical")
radio.grid(row=0, column=2, padx=2, pady=2, sticky="w")
radio=Radiobutton(master, text="horizontal", variable=self.orientation,
value="horizontal")
radio.grid(row=1, column=2, padx=2, pady=2, sticky="w")
def validate(self):
try:
if (string.atoi(self.root.get())<0 or
string.atoi(self.root.get()) not in self.G.vertices):
raise rootError
seGato/Gato.py 0100755 0017676 0017534 00000147700 10014153350 0014270 0 ustar 00schliep catbox 0000305 0050066 #!/usr/bin/env python2.2
################################################################################
#
# This file is part of Gato (Graph Animation Toolbox)
#
# file: Gato.py
# author: Alexander Schliep (schliep@molgen.mpg.de)
#
# Copyright (C) 1998-2003, Alexander Schliep, Winfried Hochstaettler and
# ZAIK/ZPR, Universitaet zu Koeln
#
# Contact: schliep@molgen.mpg.de, wh@zpr.uni-koeln.de
#
# Information: http://gato.sf.net
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Library General Public
# License as published by the Free Software Foundation; either
# version 2 of the License, or (at your option) any later version.
#
# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Library General Public License for more details.
#
# You should have received a copy of the GNU Library General Public
# License along with this library; if not, write to the Free
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#
#
#
#
# This file is version $Revision: 1.37 $
# from $Date: 2004/02/11 18:36:01 $
# last change by $Author: schliep $.
#
################################################################################
import sys
import tempfile
import traceback
import os
import bdb
import whrandom
import re
import string
import StringIO
import tokenize
import tkFont
import copy
import Gred
from Tkinter import *
from tkFileDialog import askopenfilename, asksaveasfilename
from tkMessageBox import askokcancel, showerror, askyesno
from ScrolledText import ScrolledText
from GatoConfiguration import GatoConfiguration
from Graph import Graph
from GraphUtil import *
from GraphDisplay import GraphDisplayToplevel
from GatoUtil import *
from GatoGlobals import *
from GatoDialogs import AboutBox, SplashScreen, HTMLViewer
import GatoIcons
import GatoSystemConfiguration
# put someplace else
def WMExtrasGeometry(window):
""" Returns (top,else) where
- top is the amount of extra pixels the WM puts on top
of the window
- else is the amount of extra pixels the WM puts everywhere
else around the window
NOTE: Does not work with tk8.0 style menus, since those are
handled by WM (according to Tk8.1 docs)
NOTE: Some window managers return bad geometry definition
Handle in caller
"""
try:
window.geometry() # XXX Sometimes first produced wrong results ...
g = string.split(window.geometry(),"+")
except TclError:
# bad geometry specifier: e.g. ... "-1949x260+1871+1"
return (32,32)
trueRootx = string.atoi(g[1])
trueRooty = string.atoi(g[2])
rootx = window.winfo_rootx() # top left of our window
rooty = window.winfo_rooty() # *WITHOUT* WM extras
topWMExtra = abs(rooty - trueRooty) # WM adds that on top
WMExtra = abs(rootx - trueRootx) # and that on all other sides
# XXX KLUDGE topWMExtra,WMExtra should always be in 0...32 pixels, or?
topWMExtra = min(32,topWMExtra)
WMExtra = min(32, WMExtra)
return (topWMExtra,WMExtra)
################################################################################
#
#
# Public Methods of class AlgoWin
#
# ShowActive(lineNo) Display line lineNo as activated
#
# ShowBreakpoint(lineNo) Show breakpoint at line lineNo
#
# HideBreakpoint(lineNo) Hide breakpoint at line lineNo
#
# WaitNextEvent() Wait for some GUI event
#
# WaitTime(delay) Wait for delay (in ms)
#
class AlgoWin(Frame):
""" Provide GUI with main menubar for displaying and controlling
algorithms and the algorithm text widget """
def __init__(self, parent=None):
Frame.__init__(self,parent)
#XXX import tkoptions
#tkoptions.tkoptions(self)
Splash = SplashScreen(self.master)
self.algoFont = "Courier"
self.algoFontSize = 10
self.keywordsList = [
"del", "from", "lambda", "return",
"and", "elif", "global", "not", "try",
"break", "else", "if", "or", "while",
"class", "except", "import", "pass",
"continue", "finally", "in", "print",
"def", "for", "is", "raise"]
GatoIcons.Init()
self.config = GatoConfiguration(self)
self.gatoInstaller=GatoSystemConfiguration.GatoInstaller()
# Create widgets
self.pack()
self.pack(expand=1,fill=BOTH) # Makes menuBar and toolBar sizeable
self.makeMenuBar()
self.makeAlgoTextWidget()
self.makeToolBar()
self.master.title("Gato 0.98J - Algorithm")
self.master.iconname("Gato 0.98J")
self.algorithm = Algorithm()
self.algorithm.SetGUI(self) # So that algorithm can call us
self.graphDisplay = GraphDisplayToplevel()
self.secondaryGraphDisplay = None
self.AboutAlgorithmDialog = None
self.AboutGraphDialog = None
self.lastActiveLine = 0
self.algorithmIsRunning = 0 # state
self.commandAfterStop = None # command to call after forced Stop
self.goOn = IntVar() # lock variable to avoid busy idling
self.master.protocol('WM_DELETE_WINDOW',self.Quit) # Handle WM Kills
Splash.Destroy()
# Fix focus and stacking
if os.name == 'nt' or os.name == 'dos':
self.graphDisplay.tkraise()
self.master.tkraise()
self.master.focus_force()
else:
self.tkraise()
# Make AlgoWins requested size its minimal size to keep
# toolbar from vanishing when changing window size
# Packer has been running due to splash screen
wmExtras = WMExtrasGeometry(self.graphDisplay)
width = self.master.winfo_reqwidth()
height = self.master.winfo_reqheight()
# XXX Some WM + packer combinatios ocassionally produce absurd requested sizes
width = min(600, self.master.winfo_reqwidth())
height = min(750, self.master.winfo_reqheight())
if os.name == 'nt' or os.name == 'dos':
self.master.minsize(width, height + wmExtras[1])
else: # Unix & Mac
self.master.minsize(width, height + wmExtras[0] + wmExtras[1])
self.BindKeys(self.master)
self.BindKeys(self.graphDisplay)
self.SetFromConfig() # Set values read in config
############################################################
#
# Create GUI
#
def makeMenuBar(self):
""" *Internal* Now using Tk 8.0 style menues """
self.menubar = Menu(self, tearoff=0)
# Add file menu
self.fileMenu = Menu(self.menubar, tearoff=0)
self.fileMenu.add_command(label='Open Algorithm...',
command=self.OpenAlgorithm)
self.fileMenu.add_command(label='Open Graph...',
command=self.OpenGraph)
self.fileMenu.add_command(label='New Graph...',
command=self.NewGraph)
self.fileMenu.add_command(label='Open GatoFile...',
command=self.OpenGatoFile)
#self.fileMenu.add_command(label='Save GatoFile...',
# command=self.SaveGatoFile)
self.fileMenu.add_command(label='Reload Algorithm & Graph',
command=self.ReloadAlgorithmGraph)
self.fileMenu.add_command(label='Export Graph as EPS...',
command=self.ExportEPSF)
self.fileMenu.add_separator()
self.fileMenu.add_command(label='Preferences...',
command=self.Preferences)
self.gatoInstaller.addMenuEntry(self.fileMenu)
self.fileMenu.add_separator()
self.fileMenu.add_command(label='Quit',
command=self.Quit)
self.menubar.add_cascade(label="File", menu=self.fileMenu,
underline=0)
# Add window menu
self.windowMenu=Menu(self.menubar, tearoff=0)
self.windowMenu.add_command(label='One graph window',
command=self.OneGraphWindow)
self.windowMenu.add_command(label='Two graph windows',
command=self.TwoGraphWindow)
self.menubar.add_cascade(label="Window Layout", menu=self.windowMenu,
underline=0)
# On a Mac we put our about box under the Apple menu ...
if os.name == 'mac':
self.apple=Menu(self.menubar, tearoff=0, name='apple')
self.apple.add_command(label='About Gato',
command=self.AboutBox)
self.apple.add_command(label='Help',
command=self.HelpBox)
self.apple.add_separator()
self.apple.add_command(label='About Algorithm',
command=self.AboutAlgorithm)
self.apple.add_command(label='About Graph',
command=self.AboutGraph)
self.menubar.add_cascade(menu=self.apple)
else: # ... on other systems we add a help menu
self.helpMenu=Menu(self.menubar, tearoff=0, name='help')
self.helpMenu.add_command(label='About Gato',
command=self.AboutBox)
self.helpMenu.add_command(label='Help',
command=self.HelpBox)
self.helpMenu.add_separator()
self.helpMenu.add_command(label='About Algorithm',
command=self.AboutAlgorithm)
self.helpMenu.add_command(label='About Graph',
command=self.AboutGraph)
self.menubar.add_cascade(label="Help", menu=self.helpMenu,
underline=0)
self.master.configure(menu=self.menubar)
def makeToolBar(self):
""" *Internal* Creates Start/Stop/COntinue ... toolbar """
toolbar = Frame(self, cursor='hand2', relief=FLAT)
toolbar.pack(side=BOTTOM, fill=X) # Allows horizontal growth
toolbar.columnconfigure(5,weight=1)
if os.name == 'nt' or os.name == 'dos':
px = 0
py = 0
else: # Unix
px = 0
py = 3
self.buttonStart = Button(toolbar, width=8, padx=px, pady=py,
text='Start', command=self.CmdStart)
self.buttonStep = Button(toolbar, width=8, padx=px, pady=py,
text='Step', command=self.CmdStep)
self.buttonTrace = Button(toolbar, width=8, padx=px, pady=py,
text='Trace', command=self.CmdTrace)
self.buttonContinue = Button(toolbar, width=8, padx=px, pady=py,
text='Continue', command=self.CmdContinue)
self.buttonStop = Button(toolbar, width=8, padx=px, pady=py,
text='Stop', command=self.CmdStop)
self.buttonStart.grid(row=0, column=0, padx=2, pady=2)
self.buttonStep.grid(row=0, column=1, padx=2, pady=2)
self.buttonTrace.grid(row=0, column=2, padx=2, pady=2)
self.buttonContinue.grid(row=0, column=3, padx=2, pady=2)
self.buttonStop.grid(row=0, column=4, padx=2, pady=2)
self.buttonStart['state'] = DISABLED
self.buttonStep['state'] = DISABLED
self.buttonTrace['state'] = DISABLED
self.buttonContinue['state'] = DISABLED
self.buttonStop['state'] = DISABLED
def makeAlgoTextWidget(self):
""" *Internal* Here we also define appearance of
- interactive lines
- breakpoints
- the active line """
borderFrame = Frame(self, relief=SUNKEN, bd=2) # Extra Frame
# around widget needed for more Windows-like appearance
self.algoText = ScrolledText(borderFrame, relief=FLAT,
padx=3, pady=3,
background="white", wrap='none',
width=43, height=30,
)
self.SetAlgorithmFont(self.algoFont, self.algoFontSize)
self.algoText.pack(expand=1, fill=BOTH)
borderFrame.pack(side=TOP, expand=1, fill=BOTH)
# GUI-related tags
self.algoText.tag_config('Interactive', foreground='#009900',background="#E5E5E5")
self.algoText.tag_config('Break', foreground='#ff0000',background="#E5E5E5")
self.algoText.tag_config('Active', background='#bbbbff')
self.algoText.bind("", self.handleMouse)
self.algoText['state'] = DISABLED
def SetAlgorithmFont(self, font, size):
self.algoFont = font
self.algoFontSize = size
f = tkFont.Font(self, (font, size, tkFont.NORMAL))
bf = tkFont.Font(self, (font, size, tkFont.BOLD))
itf = tkFont.Font(self, (font, size, tkFont.ITALIC))
self.algoText.config(font=f)
# syntax highlighting tags
self.algoText.tag_config('keyword', font=bf)
self.algoText.tag_config('string', font=itf)
self.algoText.tag_config('comment', font=itf)
self.algoText.tag_config('identifier', font=bf)
def SetFromConfig(self):
c = self.config.get # Shortcut to accessor
self.SetAlgorithmFont(c('algofont'), int(c('algofontsize')))
self.algoText.config(fg=c('algofg'), bg=c('algobg'))
self.algoText.tag_config('Interactive',
foreground=c('interactivefg'),
background=c('interactivebg'))
self.algoText.tag_config('Break',
foreground=c('breakpointfg'),
background=c('breakpointbg'))
self.algoText.tag_config('Active',
foreground=c('activefg'),
background=c('activebg'))
globals()['gBlinkRate'] = int(c('blinkrate'))
globals()['gBlinkRepeat'] = int(c('blinkrepeat'))
def OpenSecondaryGraphDisplay(self):
""" Pops up a second graph window """
if self.secondaryGraphDisplay == None:
self.secondaryGraphDisplay = GraphDisplayToplevel()
self.BindKeys(self.secondaryGraphDisplay)
else:
self.secondaryGraphDisplay.Show()
def WithdrawSecondaryGraphDisplay(self):
""" Hide window containing second graph """
if self.secondaryGraphDisplay != None:
self.secondaryGraphDisplay.Withdraw()
############################################################
#
# GUI Helpers
#
# Lock
def touchLock(self):
""" *Internal* The lock (self.goOn) is a variable which
is used to control the flow of the programm and to
allow GUI interactions without busy idling.
The following methods wait for the lock to be touched:
- WaitNextEvent
- WaitTime
The following methods touch it:
- CmdStop
- CmdStep
- CmdContinue """
self.goOn.set(self.goOn.get() + 1) #XXX possible overflow
def activateMenu(self):
""" Make the menu active (i.e., after stopping an algo) """
self.menubar.entryconfigure(0, state = NORMAL)
def deactivateMenu(self):
""" Make the menu inactive (i.e., before running an algo) """
self.menubar.entryconfigure(0, state = DISABLED)
def tagLine(self, lineNo, tag):
""" Add tag 'tag' to line lineNo """
self.algoText.tag_add(tag,'%d.0' % lineNo,'%d.0' % (lineNo + 1))
def unTagLine(self, lineNo, tag):
""" Remove tag 'tag' from line lineNo """
self.algoText.tag_remove(tag,'%d.0' % lineNo,'%d.0' % (lineNo + 1))
def tagLines(self, lines, tag):
""" Tag every line in list lines with specified tag """
for l in lines:
self.tagLine(l, tag)
def tokenEater(self, type, token, (srow, scol), (erow, ecol), line):
#log.debug("%d,%d-%d,%d:\t%s\t%s" % \
# (srow, scol, erow, ecol, type, repr(token)))
if type == 1: # Name
if token in self.keywordsList:
self.algoText.tag_add('keyword','%d.%d' % (srow, scol),
'%d.%d' % (erow, ecol))
elif type == 3: # String
self.algoText.tag_add('string','%d.%d' % (srow, scol),
'%d.%d' % (erow, ecol))
elif type == 39: # Comment
self.algoText.tag_add('comment','%d.%d' % (srow, scol),
'%d.%d' % (erow, ecol))
############################################################
#
# Menu Commands
#
# The menu commands are passed as call back parameters to
# the menu items.
#
def OpenAlgorithm(self,file=""):
""" GUI to allow selection of algorithm to open
file parameter for testing purposes """
if self.algorithmIsRunning:
self.CmdStop()
self.commandAfterStop = self.OpenAlgorithm
return
if file == "": # caller did not specify file
file = askopenfilename(title="Open Algorithm",
defaultextension=".py",
filetypes = [ ("Gato Algorithm", ".alg")
,("Python Code", ".py")
]
)
if file is not "":
try:
self.algorithm.Open(file)
except (EOFError, IOError):
self.HandleFileIOError("Algorithm",file)
return
self.algoText['state'] = NORMAL
self.algoText.delete('0.0', END)
self.algoText.insert('0.0', self.algorithm.GetSource())
self.algoText['state'] = DISABLED
self.tagLines(self.algorithm.GetInteractiveLines(), 'Interactive')
self.tagLines(self.algorithm.GetBreakpointLines(), 'Break')
# Syntax highlighting
tokenize.tokenize(StringIO.StringIO(self.algorithm.GetSource()).readline,
self.tokenEater)
if self.algorithm.ReadyToStart():
self.buttonStart['state'] = NORMAL
self.master.title("Gato 0.98J - " + stripPath(file))
if self.AboutAlgorithmDialog:
self.AboutAlgorithmDialog.Update(self.algorithm.About(),"About Algorithm")
def NewGraph(self):
Gred.Start()
def OpenGraph(self,file=""):
""" GUI to allow selection of graph to open
file parameter for testing purposes """
if self.algorithmIsRunning:
self.CmdStop()
self.commandAfterStop = self.OpenGraph
return
if file == "": # caller did not specify file
file = askopenfilename(title="Open Graph",
defaultextension=".gato",
filetypes = [ ("Gred", ".cat")
#,("Gato Plus", ".cat")
#,("LEDA", ".gph")
#,("Graphlet", ".let")
#,("Gato",".gato")
]
)
if file is not "":
try:
self.algorithm.OpenGraph(file)
except (EOFError, IOError):
self.HandleFileIOError("Graph",file)
return
if self.algorithm.ReadyToStart():
self.buttonStart['state'] = NORMAL
if self.AboutGraphDialog:
self.AboutGraphDialog.Update(self.graphDisplay.About(), "About Graph")
def SaveGatoFile(self,filename=""):
"""
under Construction...
"""
import GatoFile
# ToDo
if not askyesno("Ooops...",
"...this feature is under developement.\nDo you want to proceed?"):
return
if self.algorithmIsRunning:
# variable file is lost here!
self.CmdStop()
self.commandAfterStop = self.SaveGatoFile
return
if filename == "": # caller did not specify file
filename = asksaveasfilename(title="Save Graph and Algorithm",
defaultextension=".gato",
filetypes = [ ("Gato",".gato")
#,("xml",".xml")
]
)
def OpenGatoFile(self,filename=""):
"""
menu command
"""
import GatoFile
if self.algorithmIsRunning:
# variable file is lost here!
self.CmdStop()
self.commandAfterStop = self.OpenGatoFile
return
if filename == "": # caller did not specify file
filename = askopenfilename(title="Open Graph and Algorithm",
defaultextension=".gato",
filetypes = [ ("Gato",".gato")
#,("xml",".xml")
]
)
if filename is not "":
select={}
try:
# open xml file
f=GatoFile.GatoFile(filename)
select=f.getDefaultSelection()
if not select:
# select the graph
select=f.displaySelectionDialog(self)
except GatoFile.FileException, e:
self.HandleFileIOError("GatoFile: %s"%e.reason,filename)
return
# nothing selected
if select is None:
return
# a graph is selected
if select.get("graph"):
try:
# open graph
graphStream=select["graph"].getGraphAsStringIO()
self.algorithm.OpenGraph(graphStream,
fileName="%s::%s"%(filename,
select["graph"].getName()))
except (EOFError, IOError):
self.HandleFileIOError("Gato",filename)
return
if self.algorithm.ReadyToStart():
self.buttonStart['state'] = NORMAL
if self.AboutGraphDialog:
self.AboutGraphDialog.Update(self.graphDisplay.About(), "About Graph")
# great shit! create files to get old gato running
if select.get("algorithm"):
xmlAlgorithm=select.get("algorithm")
# save last algorithm tmp_name
lastAlgoFileName=None
if hasattr(self,"tmpAlgoFileName"):
lastAlgoFileName=self.tmpAlgoFileName
lastAlgoDispalyName=None
if hasattr(self,"algoDisplayFileName"):
lastAlgoDispalyName=self.algoDisplayFileName
# provide a temporary files for algortihm and prologue
tmpFileName=tempfile.mktemp()
self.tmpAlgoFileName="%s.alg"%tmpFileName
self.algoDisplayFileName="%s::%s"%(filename,xmlAlgorithm.getName())
tmp=file(self.tmpAlgoFileName,"w")
tmp.write(xmlAlgorithm.getText())
tmp.close()
proFileName="%s.pro"%tmpFileName
tmp=file(proFileName,"w")
tmp.write(xmlAlgorithm.getProlog())
tmp.close()
# open it!
# text copied from AlgoWin.OpenAlgorithm
try:
self.algorithm.Open(self.tmpAlgoFileName)
except (EOFError, IOError):
os.remove(self.tmpAlgoFileName)
os.remove(proFileName)
self.HandleFileIOError("Algorithm",self.tmpAlgoFileName)
self.algoDisplayFileName=lastAlgoDispalyName
self.tmpAlgoFileName=lastAlgoFileName
return
# handle old tempfile
if lastAlgoFileName:
os.remove(lastAlgoFileName)
os.remove(lastAlgoFileName[:-3]+'pro')
# prepare algorithm text widget
self.algoText['state'] = NORMAL
self.algoText.delete('0.0', END)
self.algoText.insert('0.0', self.algorithm.GetSource())
self.algoText['state'] = DISABLED
self.tagLines(self.algorithm.GetInteractiveLines(), 'Interactive')
self.tagLines(self.algorithm.GetBreakpointLines(), 'Break')
# Syntax highlighting
tokenize.tokenize(StringIO.StringIO(self.algorithm.GetSource()).readline,
self.tokenEater)
# set the state
if self.algorithm.ReadyToStart():
self.buttonStart['state'] = NORMAL
self.master.title("Gato 0.98J - " + stripPath(self.algoDisplayFileName))
if self.AboutAlgorithmDialog:
# to do ... alright for xml about ?!
self.AboutAlgorithmDialog.Update(self.algorithm.About(),
"About Algorithm")
def CleanUp(self):
"""
removes the temporary files...
"""
if hasattr(self,"tmpAlgoFileName") and self.tmpAlgoFileName:
os.remove(self.tmpAlgoFileName)
os.remove(self.tmpAlgoFileName[:-3]+'pro')
def ReloadAlgorithmGraph(self):
if self.algorithmIsRunning:
self.CmdStop()
self.commandAfterStop = self.ReloadAlgorithmGraph
return
if self.algorithm.algoFileName is not "":
self.OpenAlgorithm(self.algorithm.algoFileName)
if self.algorithm.graphFileName is not "":
self.OpenGraph(self.algorithm.graphFileName)
def Preferences(self):
""" Handle editing preferences """
self.config.edit()
def ExportEPSF(self):
""" GUI to control export of EPSF file """
file = asksaveasfilename(title="Export EPSF",
defaultextension=".eps",
filetypes = [ ("Encapsulated PS", ".eps")
,("Postscript", ".ps")
]
)
if file is not "":
self.graphDisplay.PrintToPSFile(file)
def Quit(self):
if self.algorithmIsRunning:
self.CmdStop()
self.commandAfterStop = self.Quit
return
if askokcancel("Quit","Do you really want to quit?"):
Frame.quit(self)
self.CleanUp()
def OneGraphWindow(self):
""" Align windows nicely for one graph window """
self.WithdrawSecondaryGraphDisplay()
self.master.update()
if os.name == 'mac':
screenTop = 19 # Take care of menubar
else:
screenTop = 0
# Keep the AlgoWin fixed in size but move it to 0,0
(topWMExtra,WMExtra) = WMExtrasGeometry(self.graphDisplay)
pad = 1 # Some optional extra space
trueWidth = self.master.winfo_width() + 2 * WMExtra + pad
# Move AlgoWin so that the WM extras will be at 0,0
# Silly enough one hast to specify the true coordinate at which
# the window will appear
try:
self.master.geometry("+%d+%d" % (pad, screenTop + pad))
except TclError:
log.debug("OneGraphWindow: self.master.geometry failed for +%d+%d" % (pad, screenTop + pad))
log.debug("OneGraphWindow: screen= (%d * %d), extras = (%d %d)" % (
self.master.winfo_screenwidth(),
self.master.winfo_screenheight(),
WMExtra,
topWMExtra)
)
# Move graph win to take up the rest of the screen
screenwidth = self.master.winfo_screenwidth()
screenheight = self.master.winfo_screenheight() - screenTop
self.graphDisplay.geometry("%dx%d+%d+%d" % (
screenwidth - trueWidth - 2 * WMExtra - pad - 1,# see 1 below
screenheight - WMExtra - topWMExtra - pad,
trueWidth + 1 + pad,
screenTop + pad))
self.graphDisplay.update()
self.master.update()
def TwoGraphWindow(self):
""" Align windows nicely for two graph windows """
self.OpenSecondaryGraphDisplay()
self.master.update()
if os.name == 'mac':
screenTop = 19 # Take care of menubar
else:
screenTop = 0
# Keep the AlgoWin fixed in size but move it to 0,0
(topWMExtra,WMExtra) = WMExtrasGeometry(self.graphDisplay)
pad = 1 # Some optional extra space
trueWidth = self.master.winfo_width() + 2 * WMExtra + pad
# Move AlgoWin so that the WM extras will be at 0,0
# Silly enough one hast to specify the true coordinate at which
# the window will appear
self.master.geometry("+%d+%d" % (pad, screenTop + pad))
# Move GraphWins so that the are stacked dividing vertical
# space evenly and taking up as much as possible horizontally
screenwidth = self.master.winfo_screenwidth()
screenheight = self.master.winfo_screenheight() - screenTop
reqGDWidth = screenwidth - trueWidth - 2 * WMExtra - pad - 1
reqGDHeight = screenheight/2 - WMExtra - topWMExtra - pad
self.graphDisplay.geometry("%dx%d+%d+%d" % (
reqGDWidth,
reqGDHeight,
trueWidth + 1 + pad,
screenTop + pad))
self.secondaryGraphDisplay.geometry("%dx%d+%d+%d" % (
reqGDWidth,
reqGDHeight,
trueWidth + 1 + pad,
screenTop + reqGDHeight + WMExtra + topWMExtra + 2 * pad))
self.master.update()
def AboutBox(self):
d = AboutBox(self.master)
def HelpBox(self):
d = HTMLViewer(gGatoHelp, "Help", self.master)
def AboutAlgorithm(self):
d = HTMLViewer(self.algorithm.About(), "About Algorithm", self.master)
self.AboutAlgorithmDialog = d
def AboutGraph(self):
d = HTMLViewer(self.graphDisplay.About(), "About Graph", self.master)
self.AboutGraphDialog = d
############################################################
#
# Tool bar Commands
#
# The tool bar commands are passed as call back parameters to
# the tool bar buttons.
#
def CmdStart(self):
""" Command linked to toolbar 'Start' """
# self.deactivateMenu()
self.buttonStart['state'] = DISABLED
self.buttonStep['state'] = NORMAL
self.buttonTrace['state'] = NORMAL
self.buttonContinue['state'] = NORMAL
self.buttonStop['state'] = NORMAL
self.algorithmIsRunning = 1
self.algorithm.Start()
def CmdStop(self):
""" Command linked to toolbar 'Stop' """
self.algorithm.Stop()
self.clickResult = ('abort',None) # for aborting interactive
# selection of vertices/edges
self.touchLock()
def CommitStop(self):
""" Commit a stop for the GUI """
self.buttonStart['state'] = NORMAL
self.buttonStep['state'] = DISABLED
self.buttonTrace['state'] = DISABLED
self.buttonContinue['state'] = DISABLED
self.buttonStop['state'] = DISABLED
# Un-activate last line
if self.lastActiveLine != 0:
self.unTagLine(self.lastActiveLine,'Active')
self.update() # Forcing redraw
self.algorithmIsRunning = 0
if self.commandAfterStop != None:
self.commandAfterStop()
self.commandAfterStop = None
# self.activateMenu()
def CmdStep(self):
""" Command linked to toolbar 'Step' """
self.algorithm.Step()
self.clickResult = ('auto',None) # for stepping over interactive
# selection of vertices/edges
self.touchLock()
def CmdContinue(self):
""" Command linked to toolbar 'Continue' """
# Should we disable continue buton here ?
self.algorithm.Continue()
self.clickResult = ('auto',None) # for stepping over interactive
# selection of vertices/edges
self.touchLock()
def CmdTrace(self):
""" Command linked to toolbar 'Trace' """
self.algorithm.Trace()
self.touchLock()
############################################################
#
# Key commands for Tool bar Commands
#
def BindKeys(self, widget):
# self.master.bind_all screws up EPSF save dialog
widget.bind('s', self.KeyStart)
widget.bind('x', self.KeyStop)
widget.bind('', self.KeyStep)
widget.bind('c', self.KeyContinue)
widget.bind('t', self.KeyTrace)
widget.bind('b', self.KeyBreak)
if isinstance(widget,GraphDisplayToplevel):
widget.bind('r', widget.highlightLastAnimation)
else:
widget.bind('r', self.graphDisplay.highlightLastAnimation)
def KeyStart(self, event):
""" Command linked to toolbar 'Start' """
if self.buttonStart['state'] != DISABLED:
self.CmdStart()
def KeyStop(self, event):
if self.buttonStop['state'] != DISABLED:
self.CmdStop()
def KeyStep(self, event):
""" Command linked to toolbar 'Step' """
if self.buttonStep['state'] != DISABLED:
self.CmdStep()
else:
self.KeyStart(event)
def KeyContinue(self, event):
""" Command linked to toolbar 'Continue' """
if self.buttonContinue['state'] != DISABLED:
self.CmdContinue()
def KeyTrace(self, event):
""" Command linked to toolbar 'Trace' """
if self.buttonTrace['state'] != DISABLED:
self.CmdTrace()
def KeyBreak(self, event):
""" Command for toggling breakpoints """
self.algorithm.ToggleBreakpoint()
############################################################
#
# Mouse Commands
#
#
# handleMouse
def handleMouse(self, event):
""" Callback for canvas to allow toggeling of breakpoints """
currLine = string.splitfields(self.algoText.index(CURRENT),'.')[0]
self.algorithm.ToggleBreakpoint(string.atoi(currLine))
############################################################
#
# Public methods (for callbacks from algorithm)
#
def ShowActive(self, lineNo):
""" Show lineNo as active line """
if self.lastActiveLine != 0:
self.unTagLine(self.lastActiveLine,'Active')
self.lastActiveLine = lineNo
self.tagLine(lineNo,'Active')
self.algoText.yview_pickplace('%d.0' % lineNo)
self.update() # Forcing redraw
def ShowBreakpoint(self, lineNo):
""" Show lineNo as breakpoint """
self.tagLine(lineNo,'Break')
def HideBreakpoint(self, lineNo):
""" Show lineNo w/o breakpoint """
self.unTagLine(lineNo,'Break')
def WaitNextEvent(self):
""" Stop Execution until user does something. This avoids
busy idling. See touchLock() """
self.wait_variable(self.goOn)
def WaitTime(self, delay):
""" Stop Execution until delay is passed. This avoids
busy idling. See touchLock() """
self.after(delay,self.touchLock)
self.wait_variable(self.goOn)
def ClickHandler(self,type,t):
""" *Internal* Callback for GraphDisplay """
self.clickResult = (type,t)
self.touchLock()
def PickInteractive(self, type, filterChoice=None, default=None):
""" Pick a vertex or an edge (specified by 'type') interactively
GUI blocks until
- a fitting object is clicked
- the algorithm is stopped
- 'Step' is clicked which will randomly select a vertex or an
edge
filterChoice is an optional method (only argument: the vertex or edge).
It returns true if the choice is acceptable
NOTE: To avoid fatal blocks randomly selected objects are not
subjected to filterChoice
"""
self.graphDisplay.RegisterClickhandler(self.ClickHandler)
if default == "None":
self.graphDisplay.UpdateInfo("Select a " + type +
" or click 'Step' or 'Continue' for no selection")
elif default == None:
self.graphDisplay.UpdateInfo("Select a " + type +
" or click 'Step' or 'Continue' for random selection")
else:
self.graphDisplay.UpdateInfo("Select a " + type +
" or click 'Step' or 'Continue' for default selection")
self.clickResult = (None,None)
goOn = 1
while goOn == 1:
self.wait_variable(self.goOn)
if self.clickResult[0] == type:
if filterChoice != None:
if filterChoice(self.clickResult[1]):
goOn = 0
else:
goOn = 0
if self.clickResult[0] in ['abort','auto']:
goOn = 0
self.graphDisplay.UnregisterClickhandler()
self.graphDisplay.DefaultInfo()
if self.clickResult[0] == 'auto':
return None
else:
return self.clickResult[1]
def HandleFileIOError(self, fileDescription, fileName):
log.error("%s file named %s produced an error" % (fileDescription, fileName))
# Endof: AlgoWin ---------------------------------------------------------------
class AlgorithmDebugger(bdb.Bdb):
"""*Internal* Bdb subclass to allow debugging of algorithms
REALLY UGLY CODE: Written before I understood the Debugger.
Probably should use sys.settrace() directly with the different
modes of debugging encoded in appropriate methods"""
def __init__(self,dbgGUI):
""" *Internal* dbgGUI is the GUI for the debugger """
self.GUI = dbgGUI
bdb.Bdb.__init__(self)
self.doTrace = 0
self.lastLine = -1
def dispatch_line(self, frame):
""" *Internal* Only dispatch if we are in the algorithm file """
fn = frame.f_code.co_filename
if fn != self.GUI.algoFileName:
return None
line = self.currentLine(frame)
if line == self.lastLine:
return self.trace_dispatch
self.lastLine = line
self.user_line(frame)
if self.quitting:
raise bdb.BdbQuit
return self.trace_dispatch
def dispatch_call(self, frame, arg):
fn = frame.f_code.co_filename
line = self.currentLine(frame)
doTrace = self.doTrace # value of self.doTrace might change
# No tracing of functions defined outside of our algorithmfile
if fn != self.GUI.algoFileName:
return None
#import inspect
#log.debug("dispatch_call %s %s %s %s %s %s" % (fn, line, frame, self.stop_here(frame), self.break_anywhere(frame), self.break_here(frame)))
#log.debug("%s" % inspect.getframeinfo(frame))
frame.f_locals['__args__'] = arg
if self.botframe is None:
# First call of dispatch since reset()
self.botframe = frame
return self.trace_dispatch
#if self.stop_here(frame) or self.break_anywhere(frame):
# return self.trace_dispatch
self.user_call(frame, arg)
if self.quitting: raise bdb.BdbQuit
if doTrace == 1:
self.doTrace = 0
return self.trace_dispatch
if self.break_anywhere(frame):
self.doTrace = 0
return self.trace_nofeedback_dispatch
return None
def trace_nofeedback_dispatch(self, frame, event, arg):
if self.quitting:
return # None
if event == 'line':
line = self.currentLine(frame)
if line in self.GUI.breakpoints:
self.GUI.mode = 2
return self.dispatch_line(frame)
else:
return None
if event == 'call':
return self.dispatch_call(frame, arg)
if event == 'return':
return self.dispatch_return(frame, arg)
if event == 'exception':
return self.dispatch_exception(frame, arg)
log.debug("bdb.Bdb.dispatch: unknown debugging event: %s" % event)
def reset(self):
""" *Internal* Put debugger into initial state, calls forget() """
bdb.Bdb.reset(self)
self.forget()
def forget(self):
self.lineno = None
self.stack = []
self.curindex = 0
self.curframe = None
def setup(self, f, t):
#self.forget()
self.stack, self.curindex = self.get_stack(f, t)
self.curframe = self.stack[self.curindex][0]
def user_call(self, frame, argument_list):
""" *Internal* This function is called when we stop or break
at this line """
line = self.currentLine(frame)
# log.debug("*user_call* %s %s" % (line, argument_list))
if self.doTrace == 1:
line = self.currentLine(frame)
if line in self.GUI.breakpoints:
self.GUI.mode = 2
self.GUI.GUI.ShowActive(line)
# TO Avoid multiple steps in def line of called fun
#self.interaction(frame, None)
self.doTrace = 0
else:
pass
def user_line(self, frame):
""" *Internal* This function is called when we stop or break at this line """
self.doTrace = 0 # XXX
line = self.currentLine(frame)
# log.debug("*user_line* %s" % line)
if line in self.GUI.breakpoints:
self.GUI.mode = 2
self.GUI.GUI.ShowActive(line)
self.interaction(frame, None)
def user_return(self, frame, return_value):
""" *Internal* This function is called when a return trap is set here """
frame.f_locals['__return__'] = return_value
#log.debug('--Return--')
#self.doTrace = 0 #YYY
# TO Avoid multiple steps in return line of called fun
#self.interaction(frame, None)
def user_exception(self, frame, (exc_type, exc_value, exc_traceback)):
""" *Internal* This function is called if an exception occurs,
but only if we are to stop at or just below this level """
frame.f_locals['__exception__'] = exc_type, exc_value
if type(exc_type) == type(''):
exc_type_name = exc_type
else: exc_type_name = exc_type.__name__
#log.debug("exc_type_name: %s" repr.repr(exc_value))
self.interaction(frame, exc_traceback)
def interaction(self, frame, traceback):
""" *Internal* This function does all the interaction with the user
depending on self.GUI.mode
- Step (self.GUI.mode == 2)
- Quit (self.GUI.mode == 0)
- Auto-run w/timer (self.GUI.mode == 1)"""
self.setup(frame, traceback)
#
#line = self.currentLine(frame)
if self.GUI.mode == 2:
old = self.GUI.mode
self.GUI.GUI.WaitNextEvent() # user event -- might change self.GUI.mode
#log.debug("self.GUI.mode: %s -> %s " % (old, self.GUI.mode))
#if self.GUI.mode == 2:
#self.do_next()
if self.GUI.mode == 0:
self.do_quit()
return # Changed
if self.GUI.mode == 1:
self.GUI.GUI.WaitTime(10) # timer event was 100
#self.do_next()
self.forget()
def do_next(self):
self.set_next(self.curframe)
def do_quit(self):
self.set_quit()
def currentLine(self, frame):
""" *Internal* returns the current line number """
return frame.f_lineno
# Endof: AlgorithmDebugger ----------------------------------------------------
class Algorithm:
""" Provides all services necessary to load an algorithm, run it
and provide facilities for visualization """
def __init__(self):
self.DB = AlgorithmDebugger(self)
self.source = "" # Source as a big string
self.interactive = []
self.breakpoints = [] # Doesnt debugger take care of it ?
self.algoFileName = ""
self.graphFileName = ""
self.mode = 0
# mode = 0 Stop
# mode = 1 Running
# mode = 2 Stepping
self.graph = None # graph for the algorithm
self.cleanGraphCopy = None # this is the backup of the graph
self.graphIsDirty = 0 # If graph was changed by running
self.algoGlobals = {} # Sandbox for Algorithm
self.logAnimator = 0
self.about = None
self.commentPattern = re.compile('[ \t]*#')
self.blankLinePattern = re.compile('[ \t]*\n')
def SetGUI(self, itsGUI):
""" Set the connection to its GUI """
self.GUI = itsGUI
def Open(self,file):
""" Read in an algorithm from file. """
self.ClearBreakpoints()
self.algoFileName = file
input=open(file, 'r')
self.source = input.read()
input.close()
# Now read in the prolog as a module to get access to the following data
# Maybe should obfuscate the names ala xxx_, have one dict ?
try:
input = open(os.path.splitext(self.algoFileName)[0] + ".pro", 'r')
options = self.ReadPrologOptions(input)
input.close()
except EOFError, IOError:
self.GUI.HandleFileIOError("Prolog",file)
return
try:
self.breakpoints = options['breakpoints']
except:
self.breakpoints = []
try:
self.interactive = options['interactive']
except:
self.interactive = []
try:
self.graphDisplays = options['graphDisplays']
except:
self.graphDisplays = None
try:
self.about = options['about']
except:
self.about = None
if self.graphDisplays != None:
if self.graphDisplays == 1 and hasattr(self,"GUI"):
self.GUI.WithdrawSecondaryGraphDisplay()
def ReadPrologOptions(self, file):
""" Prolog files should contain the following variables:
- breakpoints = [] a list of line numbers which are choosen as default
breakpoints
- interactive = [] a list of line numbers which contain interactive commands
(e.g., PickVertex)
- graphDisplays = 1 | 2 the number of graphDisplays needed by the algorithm
- about = \"\"\"\"\"\" information about the algorithm
Parameter: filelike object
"""
import re
import sys
text = file.read()
options = {}
optionPattern = {'breakpoints':'breakpoints[ \t]*=[ \t]*(\[[^\]]+\])',
'interactive':'interactive[ \t]*=[ \t]*(\[[^\]]+\])',
'graphDisplays':'graphDisplays[ \t]*=[ \t]*([1-2])'}
# about is more complicated
for patternName in optionPattern.keys():
compPattern = re.compile(optionPattern[patternName])
match = compPattern.search(text)
if match != None:
options[patternName] = eval(match.group(1))
# Special case with about (XXX: assuming about = """ ... """)
try:
aboutStartPat = re.compile('about[ \t]*=[ \t]*"""')
aboutEndPat = re.compile('"""')
left = aboutStartPat.search(text).end()
right = aboutEndPat.search(text, left).start()
options['about'] = text[left:right]
except:
pass
return options
def About(self):
""" Return a HTML-page giving information about the algorithm """
if self.about != None:
return self.about
else:
return " No information available
"
def OpenGraph(self,file,fileName=None):
""" Read in a graph from file and open the display """
if type(file) in types.StringTypes:
self.graphFileName = file
elif type(file)==types.FileType or issubclass(file.__class__,StringIO.StringIO):
self.graphFileName = fileName
else:
raise Exception("wrong types in argument list: expected string or file like object")
self.cleanGraphCopy = OpenCATBoxGraph(file)
self.restoreGraph()
self.GUI.graphDisplay.Show() # In case we are hidden
self.GUI.graphDisplay.ShowGraph(self.graph, stripPath(self.graphFileName))
self.GUI.graphDisplay.RegisterGraphInformer(WeightedGraphInformer(self.graph))
def restoreGraph(self):
self.graph=copy.deepcopy(self.cleanGraphCopy)
self.graphIsDirty = 0
def OpenSecondaryGraph(self,G,title,informer=None):
""" Read in graph from file and open the the second display """
self.GUI.OpenSecondaryGraphDisplay()
self.GUI.secondaryGraphDisplay.ShowGraph(G, title)
if informer != None:
self.GUI.secondaryGraphDisplay.RegisterGraphInformer(informer)
def ReadyToStart(self):
""" Return 1 if we are ready to run. That is when we user
has opened both an algorithm and a graph. """
if self.graphFileName != "" and self.algoFileName != "":
return 1
else:
return 0
def Start(self):
""" Start an loaded algorithm. It firsts execs the prolog and
then starts the algorithm in the debugger. The algorithms
globals (i.e., the top-level locals are in a dict we supply
and for which we preload the packages we want to make available)"""
if self.graphIsDirty == 1:
self.restoreGraph()
# Does show
self.GUI.graphDisplay.Show() # In case we are hidden
self.GUI.graphDisplay.ShowGraph(self.graph, stripPath(self.graphFileName))
self.GUI.graphDisplay.RegisterGraphInformer(WeightedGraphInformer(self.graph))
else:
self.GUI.graphDisplay.Show() # In case we are hidden
self.graphIsDirty = 1
self.mode = 1
# Set global vars ...
self.algoGlobals = {}
self.algoGlobals['self'] = self
self.algoGlobals['G'] = self.graph
if self.logAnimator:
self.algoGlobals['A'] = MethodLogger(self.GUI.graphDisplay)
else:
self.algoGlobals['A'] = self.GUI.graphDisplay
# XXX
# explictely loading packages we want to make available to the algorithm
modules = ['DataStructures',
'AnimatedDataStructures',
'AnimatedAlgorithms',
'GraphUtil',
'GatoUtil']
for m in modules:
exec("from %s import *" % m, self.algoGlobals, self.algoGlobals)
# transfer required globals
self.algoGlobals['gInteractive'] = globals()['gInteractive']
# Read in prolog and execute it
try:
execfile(os.path.splitext(self.algoFileName)[0] + ".pro",
self.algoGlobals, self.algoGlobals)
except:
log.exception("Bug in %s.pro" % os.path.splitext(self.algoFileName)[0])
#traceback.print_exc()
# Read in algo and execute it in the debugger
file = self.algoFileName
# Filename must be handed over in a very safe way
# because of \ and ~1 under windows
self.algoGlobals['_tmp_file']=self.algoFileName
# Switch on all shown breakpoints
for line in self.breakpoints:
self.DB.set_break(self.algoFileName,line)
try:
command = "execfile(_tmp_file)"
self.DB.run(command, self.algoGlobals, self.algoGlobals)
except:
log.exception("Bug in %s" % self.algoFileName)
#traceback.print_exc()
self.GUI.CommitStop()
def Stop(self):
self.mode = 0
def Step(self):
self.DB.doTrace = 0
self.mode = 2
def Continue(self):
self.DB.doTrace = 0
self.mode = 1
def Trace(self):
self.mode = 2
self.DB.doTrace = 1
def ClearBreakpoints(self):
""" Clear all breakpoints """
for line in self.breakpoints:
self.GUI.HideBreakpoint(line)
self.DB.clear_break(self.algoFileName,line)
self.breakpoints = []
def SetBreakpoints(self, list):
""" SetBreakpoints is depreciated
NOTE: Use 'breakpoint' var in prolog instead.
Set all breakpoints in list: So an algorithm prolog
can set a bunch of pre-assigned breakpoints at once """
log.info("SetBreakpoints() is depreciated. Use 'breakpoint' var in prolog instead. ")
for line in list:
self.GUI.ShowBreakpoint(line)
self.breakpoints.append(line)
self.DB.set_break(self.algoFileName,line)
def ToggleBreakpoint(self,line = None):
""" If we have a breakpoint on line, delete it, else add it.
If no line is passed we ask the DB for it"""
if line == None:
line = self.DB.lastLine
if line in self.breakpoints:
self.GUI.HideBreakpoint(line)
self.breakpoints.remove(line)
self.DB.clear_break(self.algoFileName,line)
else: # New Breakpoint
# check for not breaking in comments nor on empty lines.
import linecache
codeline = linecache.getline(self.algoFileName,line)
if codeline != '' and self.commentPattern.match(codeline) == None and self.blankLinePattern.match(codeline) == None:
self.GUI.ShowBreakpoint(line)
self.breakpoints.append(line)
self.DB.set_break(self.algoFileName,line)
def GetInteractiveLines(self):
""" Return lines on which user interaction (e.g., choosing a
vertex occurrs. """
return self.interactive
def GetBreakpointLines(self):
""" Return lines on which user interaction (e.g., choosing a
vertex occurrs. """
return self.breakpoints
def GetSource(self):
""" Return the algorithms source """
return self.source
def NeededProperties(self, propertyValueDict):
""" Check that graph has that value for each property
specified in the dictionary 'propertyValueDict'
If check fails algorithm is stopped
Proper names for properties are defined in gProperty """
for property in propertyValueDict.keys():
value = self.graph.Property(property)
if value != propertyValueDict[property]:
r = askokcancel("Gato - Error",
"The algorithm you started requires that the graph " +
"it works on has certain properties. The graph does " +
"not have the correct value " +
"for the property '" + property + "'.\n" +
"Do you still want to proceed ?")
if not r:
self.GUI.CmdStop()
def PickVertex(self, default=None, filter=None, visual=None):
""" Pick a vertex interactively.
- default: specifies the vertex returned when user does not
want to select one. If default==None, a random
vertex not subject to filter will be returned.
- filter: a function which should return a non-None value
if the passed vertex is acceptable
- visual is a function which takes the vertex as its
only argument and cause e.g. some visual feedback """
v = None
#log.debug("pickVertex %s" %s globals()['gInteractive'])
if globals()['gInteractive'] == 1:
v = self.GUI.PickInteractive('vertex', filter, default)
if v == None:
if default == None:
v = whrandom.choice(self.graph.vertices)
else:
v = default
if visual is not None:
visual(v)
return v
def PickEdge(self, default=None, filter=None, visual=None):
""" Pick an edge interactively
- default: specifies the edge returned when user does not
want to select one. If default==None, a random
edge not subject to filter will be returned
- filter: a function which should return a non-None value
if the passed edge is acceptable
- visual is a function which takes the edge as its
only argument and cause e.g. some visual feedback """
e = None
if globals()['gInteractive'] == 1:
e = self.GUI.PickInteractive('edge', filter, default)
if e == None:
if default == None:
e = whrandom.choice(self.graph.Edges())
else:
e = default
if visual is not None:
visual(e)
return e
################################################################################
def usage():
print "Usage: Gato.py"
print " Gato.py -v algorithm.alg graph.cat | gato-file"
print " -v or --verbose switches on the debugging/logging information"
if __name__ == '__main__':
import getopt
try:
opts, args = getopt.getopt(sys.argv[1:], "v", ["verbose"])
except getopt.GetoptError:
usage()
sys.exit(2)
if (len(args) < 3):
import logging
log = logging.getLogger("Gato.py")
for o, a in opts:
if o in ("-v", "--verbose"):
logging.verbose = 1
log.info("Welcome! Gato logging facilities activated")
app = AlgoWin()
#======================================================================
# Gato.py
if len(args) == 2:
algorithm = args[0]
graph = args[1]
app.OpenAlgorithm(algorithm)
app.update_idletasks()
app.update()
app.OpenGraph(graph)
app.update_idletasks()
app.update()
app.after_idle(app.CmdContinue) # after idle needed since CmdStart
app.CmdStart()
app.update_idletasks()
elif len(args)==1:
# expect gato file name or url
fileName=args[0]
app.OpenGatoFile(fileName)
app.update_idletasks()
app.update()
app.mainloop()
else:
usage()
sys.exit(2)
ch = compPattern.search(text)
if match != None:
optionsGato/GatoConfiguration.py 0100644 0017676 0017534 00000021012 10014153351 0017001 0 ustar 00schliep catbox 0000305 0050066 ################################################################################
#
# This file is part of Gato (Graph Animation Toolbox)
#
# file: GatoConfiguration.py
# author: Alexander Schliep (schliep@molgen.mpg.de)
#
# Copyright (C) 1998-2003, Alexander Schliep, Winfried Hochstaettler and
# ZAIK/ZPR, Universitaet zu Koeln
#
# Contact: schliep@molgen.mpg.de, wh@zpr.uni-koeln.de
# Information: http://gato.sf.net
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Library General Public
# License as published by the Free Software Foundation; either
# version 2 of the License, or (at your option) any later version.
#
# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Library General Public License for more details.
#
# You should have received a copy of the GNU Library General Public
# License along with this library; if not, write to the Free
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#
#
#
# This file is version $Revision: 1.3 $
# from $Date: 2003/02/12 14:24:57 $
# last change by $Author: schliep $.
#
###############################################################################
import ConfigParser
import tkSimpleDialog
import sys
import os
import tkFont
from Tkinter import *
from EditObjectAttributesDialog import TkIntEntry, TkStringPopupSelector, TkColorSelector
import StringIO
default_cfg = """
[animation]
BlinkRate = 50
BlinkRepeat = 4
[algorithm]
AlgoFont = Courier
AlgoFontSize = 10
# FG determines the foreground color and BG the background
# Valid colors are Tk names or #rrggbb hex tripel
AlgoFG = black
AlgoBG = white
BreakpointFG = #ff0000
BreakpointBG = #e5e5e5
InteractiveFG = #009900
InteractiveBG = #e5e5e5
ActiveFG = black
ActiveBG = #bbbbff
"""
class GatoConfiguration:
""" GatoConfiguration provides a collection of all editable
configuration items
Configurations are read from
- default_cfg string
- gato.cfg
- ~user/.gato.cfg
"""
def __init__(self, parent):
self.parent = parent
self.config = ConfigParser.ConfigParser()
self.config.readfp(StringIO.StringIO(default_cfg))
self.config.read(['gato.cfg', os.path.expanduser('~/.gato.cfg')])
# Keys which have widgets for editing
self.editkeys = ['blinkrate', 'blinkrepeat',
'algofont', 'algofontsize',
'algofg', 'algobg',
'breakpointfg', 'breakpointbg',
'interactivefg', 'interactivebg',
'activefg', 'activebg']
# setup a list of legal config keys and remember what section
# they are from
self.keys = []
self.section = {}
for sec in self.config.sections():
self.keys += self.config.options(sec)
for opt in self.config.options(sec):
self.section[opt] = sec
#self.fonts = ['Courier', 'Helvetica']
self.fonts = tkFont.families(self.parent)
self.modified = 0
def writeBack(self):
""" write the current configuration to the user's config file """
file = os.path.expanduser('~/.gato.cfg')
try:
cf = open(file, 'w')
except:
return
self.config.write(cf)
cf.close()
def get(self, name):
if name in self.keys:
return self.config.get(self.section[name], name)
else:
raise 'NoOptionError'
def set(self, name, value):
if name in self.keys:
self.config.set(self.section[name], name, "%s" % value)
self.modified = 1
else:
raise 'NoOptionError'
def edit(self):
""" Bring up the editor for the configuration """
conf_dialog = EditPreferencesDialog(self.parent, self)
if self.modified == 1:
self.writeBack()
self.parent.SetFromConfig()
class EditPreferencesDialog(tkSimpleDialog.Dialog):
def __init__(self, master, gatoconfig):
self.widgets = {}
self.gatoconfig = gatoconfig
tkSimpleDialog.Dialog.__init__(self, master, "Preferences")
def pane(self, master, name, desc):
frame = Frame(master, relief=RIDGE, borderwidth=2)
labelframe = Frame(frame)
label = Label(labelframe, font="Helvetica 10 bold",
text=name, justify=LEFT, fg="black")
label.pack(expand=1, side=LEFT, anchor=W)
label = Label(labelframe, font="Helvetica 10 italic",
text=desc, justify=RIGHT, fg="black")
label.pack(expand=1, side=RIGHT, anchor=E)
labelframe.pack(expand=1, fill=X, side=TOP)
frame.pack(expand=1, fill=BOTH, side=TOP)
result = Frame(frame)
return result
def body(self, master):
self.resizable(0,0)
#------------------ pane ------------------------------------------
pane = self.pane(master, "Animation", "Speed and Visuals")
row = 0
Label(pane, text="Delay between instructions:").grid(row=row,
column=0, sticky=E)
self.widgets['blinkrate'] = TkIntEntry(pane, 3)
self.widgets['blinkrate'].tkWidget().grid(row=row, column=1, sticky=W)
self.widgets['blinkrate'].set(100)
row += 1
Label(pane, text="Number of blinks:").grid(row=row, column=0, sticky=E)
self.widgets['blinkrepeat'] = TkIntEntry(pane, 2)
self.widgets['blinkrepeat'].tkWidget().grid(row=row, column=1, sticky=W)
self.widgets['blinkrepeat'].set(4)
pane.pack(expand=1, fill=BOTH, padx=4, pady=4, side=BOTTOM)
#------------------ pane ------------------------------------------
pane = self.pane(master, "Algorithm", "Fonts and Colors")
row = 1
Label(pane, text="Font:").grid(row=row, column=0, sticky=E)
self.widgets['algofont'] = TkStringPopupSelector(pane, self.gatoconfig.fonts)
self.widgets['algofont'].tkWidget().grid(row=row, column=1,
sticky=W, columnspan=2)
row += 1
Label(pane, text="Font Size:").grid(row=row, column=0, sticky=E)
self.widgets['algofontsize'] = TkIntEntry(pane, 2)
self.widgets['algofontsize'].tkWidget().grid(row=row, column=1, sticky=W)
self.widgets['algofontsize'].set(12)
row += 1
Label(pane, text="Foreground").grid(row=row, column=1, sticky=W)
Label(pane, text="Background").grid(row=row, column=2, sticky=W)
row += 1
Label(pane, text="Font Color:").grid(row=row, column=0, sticky=E)
self.widgets['algofg'] = TkColorSelector(pane, 'black')
self.widgets['algofg'].tkWidget().grid(row=row, column=1, sticky=W)
self.widgets['algobg'] = TkColorSelector(pane, 'black')
self.widgets['algobg'].tkWidget().grid(row=row, column=2, sticky=W)
row += 1
Label(pane, text="Breakpoint Font Color:").grid(row=row, column=0, sticky=E)
self.widgets['breakpointfg'] = TkColorSelector(pane, 'black')
self.widgets['breakpointfg'].tkWidget().grid(row=row, column=1, sticky=W)
self.widgets['breakpointbg'] = TkColorSelector(pane, 'black')
self.widgets['breakpointbg'].tkWidget().grid(row=row, column=2, sticky=W)
row += 1
Label(pane, text="Interactive Font Color:").grid(row=row, column=0, sticky=E)
self.widgets['interactivefg'] = TkColorSelector(pane, 'black')
self.widgets['interactivefg'].tkWidget().grid(row=row, column=1, sticky=W)
self.widgets['interactivebg'] = TkColorSelector(pane, 'black')
self.widgets['interactivebg'].tkWidget().grid(row=row, column=2, sticky=W)
row += 1
Label(pane, text="Active lines:").grid(row=row, column=0, sticky=E)
self.widgets['activefg'] = TkColorSelector(pane, 'black')
self.widgets['activefg'].tkWidget().grid(row=row, column=1, sticky=W)
self.widgets['activebg'] = TkColorSelector(pane, 'black')
self.widgets['activebg'].tkWidget().grid(row=row, column=2, sticky=W)
pane.pack(expand=1, fill=BOTH, padx=4, pady=4, side=BOTTOM)
for pref in self.gatoconfig.editkeys:
self.widgets[pref].set(self.gatoconfig.get(pref))
def validate(self):
## Colors need no validating
# Everything is valid
for pref in self.gatoconfig.editkeys:
self.gatoconfig.set(pref, self.widgets[pref].get())
return 1
#---------------- Test code ---------------------------------------------
class TkTestFrame(Frame):
def __init__(self, parent=None):
Frame.__init__(self,parent)
Pack.config(self)
self.createWidgets()
#self.fonts = tkFont.families(self)
self.config = GatoConfiguration(self)
self.fonts = ['Helvetica','Times']
def createWidgets(self):
self.QUIT = Button(self, text='QUIT', foreground='red',
command=self.quit)
self.QUIT.pack(side=LEFT)
self.About = Button(self, text='Preferences', foreground='red',
command=self.About)
self.About.pack(side=LEFT)
def About(self):
self.config.edit()
if __name__ == '__main__':
app = TkTestFrame()
app.mainloop()
, master, "Preferences")
def pane(self, master, name, desc):
frame = Frame(master, relief=RIDGE, borderwidth=2)
labelframe = Frame(frame)
label = Label(labelframe, font="Helvetica 10 bold",
text=name, justify=LEFT, fg="black")
label.pack(expand=1, side=LEFT, anchor=W)
label = Label(labelframe, font="Helvetica 10 italic",
text=desc, justify=RIGHT, fg="black")
label.pack(expand=1, side=RIGHT, anchor=E)
labelframe.pack(expand=1, fill=X, side=TOP)
frame.pack(expand=1, fGato/GatoDialogs.py 0100644 0017676 0017534 00000016717 10014153351 0015574 0 ustar 00schliep catbox 0000305 0050066 ################################################################################
#
# This file is part of Gato (Graph Animation Toolbox)
#
# file: GatoDialogs.py
# author: Alexander Schliep (schliep@molgen.mpg.de)
#
# Parts of the source from this file has been taken from
# the Python Tkinter demos.
#
# This file is version $Revision: 1.23 $
# from $Date: 2004/02/11 18:36:01 $
# last change by $Author: schliep $.
#
################################################################################
from Tkinter import *
from ScrolledText import *
import GatoUtil
import GatoGlobals
import GatoIcons
import tkSimpleDialog
import sys
import os
import htmllib, formatter
# Should be in GatoGlobals
crnotice1 = "Copyright (C) 1998-2003, ZAIK/ZPR, Universität zu Köln\n"\
"Gato version 0.98J from 02/13/2004"
crnotice2 = "Written by Alexander Schliep (schliep@molgen.mpg.de).\n" \
"Application Design: Alexander Schliep and \n" \
"Winfried Hochstaettler. Additional developers: Torsten\n" \
"Pattberg, Ramazan Buzdemir, Achim Gaedke and\nWasinee Rungsarityotin." \
"Screen Design: Heidrun Krimmel.\n\n" \
"For Information see http://gato.sf.net\n" \
"Gato comes with ABSOLUTELY NO WARRANTY.\n" \
"This is free software, and you are welcome to redistribute\n" \
"it under certain conditions. For details see 'LGPL.txt'.\n"
class AboutBox(tkSimpleDialog.Dialog):
""" The application's about box """
def buttonbox(self):
# Stolen from tkSimpleDialog.py
# add standard button box. override if you don't want the
# standard buttons
box = Frame(self)
w = Button(box, text="OK", width=10, command=self.ok, default=ACTIVE)
w.pack(side=RIGHT, padx=5, pady=5)
self.bind("", self.ok)
box.pack(side=BOTTOM,fill=X)
def body(self, master):
self.resizable(0,0)
self.catIconImage = PhotoImage(data=GatoIcons.gato) # statt file=
self.catIcon = Label(master, image=self.catIconImage)
self.catIcon.pack(side=TOP)
label = Label(master, text=crnotice1)
label.pack(side=TOP)
label = Label(master, font="Helvetica 10", text=crnotice2, justify=CENTER)
label.pack(side=TOP)
color = self.config("bg")[4]
self.infoText = ScrolledText(master, relief=FLAT,
padx=3, pady=3,
background=color,
#foreground="black",
wrap='word',
width=60, height=12,
font="Times 10")
self.infoText.pack(expand=0, fill=X, side=BOTTOM)
self.infoText.delete('0.0', END)
self.infoText.insert('0.0', GatoGlobals.gLGPLText)
self.infoText.configure(state=DISABLED)
self.title("Gato - About")
class SplashScreen(Toplevel):
""" Provides a splash screen. Usage:
Subclass and override 'CreateWidgets()'
In constructor of main window/application call
- S = SplashScreen(main=self) (if caller is Toplevel)
- S = SplashScreen(main=self.master) (if caller is Frame)
- S.Destroy() after you are done creating your widgets etc.
"""
def __init__(self, master=None):
Toplevel.__init__(self, master, relief=RAISED, borderwidth=5)
self.main = master
if self.main.master != None:
self.main.master.withdraw()
self.main.withdraw()
self.overrideredirect(1)
self.CreateWidgets()
self.after_idle(self.CenterOnScreen)
self.update()
def CenterOnScreen(self):
self.update_idletasks()
xmax = self.winfo_screenwidth()
ymax = self.winfo_screenheight()
x0 = (xmax - self.winfo_reqwidth()) / 2
y0 = (ymax - self.winfo_reqheight()) / 2
self.geometry("+%d+%d" % (x0, y0))
def CreateWidgets(self):
self.catIconImage = PhotoImage(data=GatoIcons.gato) # statt file=
self.label = Label(self, image=self.catIconImage)
self.label.pack(side=TOP)
self.label = Label(self, text=crnotice1)
self.label.pack(side=TOP)
label = Label(self, font="Helvetica 10", text=crnotice2, justify=CENTER)
label.pack(side=TOP)
def Destroy(self):
self.main.update()
self.main.deiconify()
self.withdraw()
class HTMLWriter(formatter.DumbWriter):
def __init__(self, textWidget, viewer):
formatter.DumbWriter.__init__(self, self)
self.textWidget = textWidget
self.viewer = viewer
self.indent = ""
def write(self, data):
self.textWidget.insert( 'insert', data)
def new_margin(self, margin, level):
self.indent = '\t' * level
def send_label_data(self, data):
self.write(self.indent + data + ' ')
class MyHTMLParser(htmllib.HTMLParser):
""" Basic parser with image support added. output is supposed to be
the textwidget for output """
def __init__(self, formatter, output):
htmllib.HTMLParser.__init__(self, formatter)
self.output = output
def handle_image(self, source, alt, ismap, align, width, height):
imageCache = GatoUtil.ImageCache() # ImageCache is a singleton
self.output.image_create('insert', image=imageCache[source], align='baseline')
class HTMLViewer(Toplevel):
""" Basic class which provides a scrollable area for viewing HTML
text and a Dismiss button """
def __init__(self, htmlcode, title, master=None):
Toplevel.__init__(self, master)
#self.protocol('WM_DELETE_WINDOW',self.withdraw)
self.titleprefix = title
color = self.config("bg")[4]
borderFrame = Frame(self, relief=SUNKEN, bd=2) # Extra Frame
self.text = ScrolledText(borderFrame, relief=FLAT,
padx=3, pady=3,
#background='white',
background=color,
#foreground="black",
wrap='word',
width=60, height=12,
font="Times 10")
self.text.pack(expand=1, fill=BOTH)
#self.text.insert('0.0', text)
self.text['state'] = DISABLED
borderFrame.pack(side=TOP,expand=1,fill=BOTH)
box = Frame(self)
w = Button(box, text="Dismiss", width=10, command=self.withdraw, default=ACTIVE)
w.pack(side=RIGHT, padx=5, pady=5)
self.bind("", self.withdraw)
box.pack(side=BOTTOM,fill=BOTH)
self.insert(htmlcode)
def Update(self,htmlcode, title):
self.titleprefix = title
self.insert(htmlcode)
def insert(self, htmlcode):
self.text['state'] = NORMAL
self.text.delete('0.0', END)
writer = HTMLWriter(self.text, self)
format = formatter.AbstractFormatter(writer)
#parser = htmllib.HTMLParser(format)
parser = MyHTMLParser(format, self.text)
parser.feed(htmlcode)
parser.close()
self.text['state'] = DISABLED
if parser.title != None:
self.title(self.titleprefix + " - " + parser.title)
else:
self.title(self.titleprefix)
#---------------------------------- test code -----------------------------------
about = """
Breadth-First-Search
Description
This algorithm traverses a graph in breadth-first
order.
Visualisation
You see
Implementation
This was done by
asasdadasdasdaaasssssssssssssssssssssssssssssssssssssssssssssssss
Blaeh
sgmllib.
The following is a summary of the interface defined by
sgmllib.SGMLParser:
- The interface
- Its implementation
- WHat not
- The interface
- Its implementation
- WHat not
- x
- does wild things
- y
- is even wilder
"""
if __name__ == '__main__':
win = HTMLViewer(about, "Dummy")
Tk().mainloop()
s 10")
self.infoText.pack(expand=0, fill=X, sideGato/GatoFile.py 0100644 0017676 0017534 00000116254 10014153351 0015066 0 ustar 00schliep catbox 0000305 0050066 #!/usr/bin/env python2.2
################################################################################
#
# This file is part of Gato (Graph Animation Toolbox)
#
# file: GatoFile.py
# author: Achim Gaedke (achim.gaedke@zpr.uni-koeln.de)
#
# Copyright (C) 1998-2003, Alexander Schliep, Winfried Hochstaettler and
# ZAIK/ZPR, Universitaet zu Koeln
#
# Contact: schliep@molgen.mpg.de, wh@zpr.uni-koeln.de
# Information: http://gato.sf.net
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Library General Public
# License as published by the Free Software Foundation; either
# version 2 of the License, or (at your option) any later version.
#
# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Library General Public License for more details.
#
# You should have received a copy of the GNU Library General Public
# License along with this library; if not, write to the Free
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#
# This file is version $Revision: 1.3 $
# from $Date: 2003/02/12 14:24:57 $
# last change by $Author: schliep $.
#
################################################################################
import sys
import os
import types
import codecs
import StringIO
import urllib2
import xml.dom.minidom
import Gato
import Graph
import GraphUtil
import Tkinter
import TreeWidget
import TextTreeWidget
import ScrolledText
# code handling:
# the data object model module is unicode
# xml*Element functions return iso-8859-1 strings
# tkinter does not like python unicode, it gets iso-8859-1 strings
# file data are encoded to iso-8859-1
# xmlDecode(unicode) returns iso char string
(_isoEncoder,_isoDecoder,_isoStreamReader,_isoStreamWriter)=codecs.lookup("iso-8859-1")
xmlDecode=lambda x:_isoEncoder(x)[0]
# xmlEnocde(iso_char_string) returns unicode
xmlEncode=lambda x:_isoDecoder(x)[0]
# object that encodes stream data from unicode to iso-8859-1
xmlEncodedStream=_isoStreamWriter
class FileException(Exception):
"""
is the subclass for all file and xml related things that can go wrong in this module
"""
def __init__(self, reason):
"""
takes the reason or explanation as argument
"""
self.reason=reason
class GatoStorageCache:
"""
singleton list for all accessed/used/cached storage locations
"""
pass
class GatoStorageLocation:
"""
bundle of common functions for file load/save/selection
These functions act on the level of graphs and algorithms.
they propagate the open and save calls to the different file implementations
"""
def __init__(self,fileName=None):
"""
opens the file and prepares the access
"""
self.fileName=fileName
def chooseLocation(self,accessMode="rw"):
"""
opens a suitable dialog to choose a possible location for reading/writing gato data
"""
def __repr__(self):
"""
a short representation of the instance
"""
return "<%s: %s>"%(self.__class__.__name__,self.fileName)
def __str__(self):
"""
should display the name in human understandable manner
"""
return self.fileName
def readGraph(self,whichGraph=None):
"""
returns a Graph object
"""
pass
def readAlgortihm(self, whichAlgorithm=None):
"""
returns an Algorithm object
"""
pass
def writeGraph(self, Graph, whichGraph=None):
"""
puts a graph to the collection of graphs
"""
pass
def writeAlgorithm(self, Algorithm, whichAlgorithm=None):
"""
puts a graph to the collection of algorithms
"""
pass
class GatoStorageLocationTest:
"""
some tests for GatoStorageLocation
"""
def __init__(self):
l=GatoStorageLocation("bla")
print "human readable: %s, python representation %s"%(l,repr(l))
class GatoDirBranch(TreeWidget.DirBranch):
"""
"""
def updateChildList(self):
"""
create/update child list suitable for gato
"""
oldChildren=self.children
self.children=[]
entries=[]
try:
entries=os.listdir(self.path)
except OSError:
pass
entries.sort()
oldEntries=map(lambda c:c.name,oldChildren)
for entry in entries:
if entry in oldEntries:
self.children.append(oldChildren[oldEntries.index(entry)])
else:
entrypath=os.path.join(self.path,entry)
if os.path.isdir(entrypath):
self.children.append(GatoDirBranch(parent=self,name=entry))
elif os.path.isfile(entrypath):
# select suitable leaf
if entry[-5:]==".gato":
self.children.append(GatoFileBranch(parent=self,name=entry))
elif entry[-4:]==".xml":
self.children.append(xmlGatoFileBranch(parent=self,name=entry))
elif entry[-4:]==".alg" and os.path.isfile(entrypath[:-4]+".pro"):
self.children.append(algLeaf(parent=self,name=entry))
elif entry[-4:]==".cat":
self.children.append(catLeaf(parent=self,name=entry))
class GatoFileBranch(TreeWidget.xmlFileBranch):
"""
ToDo: supports xml files with gato root section
"""
pass
class xmlGatoFileBranch(TreeWidget.xmlFileBranch):
"""
ToDo supports xml files with gato sections inside
"""
pass
class algLeaf(TreeWidget.Leaf):
"""
old algortithm file
"""
def __init__(self,parent=None,name=None):
TreeWidget.Leaf.__init__(self,parent=parent, name=name)
self.selectable=1
class catLeaf(TreeWidget.Leaf):
"""
old graph file
"""
def __init__(self,parent=None,name=None):
TreeWidget.Leaf.__init__(self,parent=parent, name=name)
self.selectable=1
class GatoTree(TreeWidget.scrolledTree):
"""
"""
def __init__(self,master):
TreeWidget.scrolledTree.__init__(self,master)
self.root=GatoDirBranch(parent=self,name="/",anchor=(1,1))
self.root.display()
# show current work directory
current=os.getcwd()
currentList=[]
while current!=self.root.name:
(current,piece)=os.path.split(current)
currentList.insert(0,piece)
# expand all branches on the way
thisNode=self.root
thisNode.expand()
for piece in currentList:
thisNode=filter(lambda x:x.name==piece,thisNode.children)[0]
thisNode.expand()
(Nx0,Ny0,Nx1,Ny1)=thisNode.getBoundingBox()
(Cx0,Cy0,Cx1,Cy1)=self.bbox(Tkinter.ALL)
self.yview(Tkinter.MOVETO,float(Ny0)/float(Cy1-Cy0))
class WorkInProgress(Tkinter.Tk):
"""
my development and test class
"""
def __init__(self):
Tkinter.Tk.__init__(self)
self.tree=GatoTree(self)
self.tree.pack(expand=1,fill=Tkinter.BOTH)
def run(self):
self.mainloop()
class xmlAlgorithmElement:
algorithmElementName="algorithm"
sourceElementName="text"
prologElementName="prolog"
aboutElementName="about"
def __init__(self,dom):
# test for right tag
if dom.tagName!=self.algorithmElementName:
raise FileException("Wrong element name: %s, %s expected"
%(dom.tagName,self.algorithmElementName))
self.domElement=dom
def setAlgorithmFromALGFile(self,fileName):
"""
writes algorithm data to xml sections
"""
algorithm=Gato.Algorithm()
algorithm.Open(fileName)
self.setAlgorithm(algorithm.source)
self.setAbout(algorithm.About())
PrologFileName=fileName[:-4]+'.pro'
if os.access(PrologFileName,os.R_OK):
self.setProlog(file(PrologFileName).read())
else:
print "Warning: could not read prolog file %s"%PrologFileName
def setProlog(self,text):
"""
creates/repaces the prolog section
"""
#create new one
newProlog=None
if self.domElement.ownerDocument:
newProlog=self.domElement.ownerDocument.createElement(self.prologElementName)
newProlog.appendChild(self.domElement.ownerDocument.createCDATASection(
xmlEncode(text)
))
else:
newProlog=xml.dom.Element(self.prologElementName)
newProlog.appendChild(xml.dom.CDATASection(xmlEncode(text)))
existingProlog=self.domElement.getElementsByTagName(self.prologElementName)
if len(existingProlog)>0:
# replace existing prolog
self.domElement.repaceChild(newProlog,existingProlog[0])
else:
self.domElement.appendChild(newProlog)
def setAlgorithm(self,text):
"""
creates/repaces the algorithm section
"""
#create new one
newAlgorithm=None
if self.domElement.ownerDocument:
newAlgorithm=self.domElement.ownerDocument.createElement(self.sourceElementName)
newAlgorithm.appendChild(self.domElement.ownerDocument.createCDATASection(
xmlEncode(text)
))
else:
newAlgorithm=xml.dom.Element(self.sourceElementName)
newAlgorithm.appendChild(xml.dom.CDATASection(xmlEncode(text)))
existingAlgorithm=self.domElement.getElementsByTagName(self.sourceElementName)
if len(existingAlgorithm)>0:
# replace existing algorithm
self.domElement.repaceChild(newAlgorithm,existingAlgorithm[0])
else:
self.domElement.appendChild(newAlgorithm)
def setAbout(self,text):
"""
ToDo
"""
pass
def getTextFromElement(self,tagName):
"returns the text of specified tag"
requestedElements=self.domElement.getElementsByTagName(tagName)
if len(requestedElements)!=1:
return None
requestedElement=requestedElements[0]
text=""
for e in requestedElement.childNodes:
if (e.nodeType==xml.dom.minidom.Node.TEXT_NODE or
e.nodeType==xml.dom.minidom.Node.CDATA_SECTION_NODE):
text+=e.data
return xmlDecode(text)
def getText(self):
t=self.getTextFromElement(self.sourceElementName)
if t:
return t
else:
return ""
def getAboutAsString(self):
requestedElements=filter(lambda e:e.nodeType==xml.dom.Node.ELEMENT_NODE and \
e.tagName==self.aboutElementName,
self.domElement.childNodes)
if len(requestedElements)<1:
return None
else:
return requestedElements[0].toxml()
def getTextAsFile(self):
"""
creates a StringIO Object from file
"""
return StringIO.StringIO(self.getText())
def getProlog(self):
"returns the prolog"
p=self.getTextFromElement(self.prologElementName)
if p:
return p
else:
return ""
def getPrologAsFile(self):
return StringIO.StringIO(self.getProlog())
def getName(self):
return xmlDecode(self.domElement.getAttribute("name"))
def setName(self,name):
self.domElement.setAttribute("name",xmlEncode(name))
class xmlGraphElement:
graphElementName="graph"
def __init__(self,graph,name=None):
"""
called with a dom element: grants access to Graph
called with a graph and a name string: creates the dom structure from graph
"""
# test for right tag
if issubclass(graph.__class__,xml.dom.minidom.Element):
# we were called with a dom element as argument
if graph.tagName!=self.graphElementName:
raise FileException("Wrong element name: %s, %s expected"
%(graph.tagName,self.graphElementName))
self.domElement=graph
elif issubclass(graph.__class__,Graph.Graph) and type(name) in types.StringTypes:
# initialise dom element
self.domElement=xml.dom.minidom.Element(xmlGraphElement.graphElementName)
self.setName(name)
self.setGraph(graph)
else:
raise FileException("wrong arguments provided")
def getAboutAsString(self):
requestedElements=filter(lambda e:e.nodeType==xml.dom.Node.ELEMENT_NODE and e.tagName=="about",
self.domElement.childNodes)
if len(requestedElements)<1:
return None
else:
return requestedElements[0].toxml()
def getGraph(self):
"""
returns the Graph object constructed from this dom element
"""
return GraphUtil.OpenCATBoxGraph(self.getGraphAsStringIO())
def getGraphAsStringIO(self):
text=StringIO.StringIO()
for e in self.domElement.childNodes:
if (e.nodeType==xml.dom.minidom.Node.TEXT_NODE or
e.nodeType==xml.dom.minidom.Node.CDATA_SECTION_NODE):
text.write(xmlDecode(e.data))
text.seek(0)
return text
def setGraph(self,graph):
# we were called with a Graph and a name as arguments
data=StringIO.StringIO()
GraphUtil.SaveCATBoxGraph(graph,data)
#remove all other text nodes
textNodes=filter(lambda e:e.nodeType==xml.dom.minidom.Node.TEXT_NODE or
e.nodeType==xml.dom.minidom.Node.CDATA_SECTION_NODE,
self.domElement.childNodes)
map(self.domElement.removeChild,textNodes)
# create necessary dom elements
# maybe xml.dom.minidom.Text is ok, too
newCDATASection=None
if self.domElement.ownerDocument:
newCDATASection=self.domElement.ownerDocument.createCDATASection(
xmlEncode(data.getvalue())
)
else:
newCDATASection=xml.dom.minidom.CDATASection(xmlEncode(data.getvalue()))
self.domElement.appendChild(newCDATASection)
def setGraphFromCATFile(self,GraphFile):
"""
creates a graph object from .cat file and sets it
this detour assures an actual graph format
"""
self.setGraph(GraphUtil.OpenCATBoxGraph(GraphFile))
def getName(self):
return xmlDecode(self.domElement.getAttribute("name"))
def setName(self,name):
self.domElement.setAttribute("name",xmlEncode(name))
class xmlGatoElement:
"""
is an access and modification interface to the gato dom structure
"""
gatoElementName="gato"
def __init__(self,_domElement):
"""
creates the access and modification interface
"""
# test for right tag
if _domElement.tagName!=self.gatoElementName:
raise FileException("Wrong element name: %s, %s expected"
%(_domElement.tagName,self.gatoElementName))
self.domElement=_domElement
def createGraphElement(self,name=None):
"""
creates a new graph Element
"""
newDOMElement=None
if self.domElement.ownerDocument:
newDOMElement=self.domElement.ownerDocument.createElement(
xmlGraphElement.graphElementName)
else:
newDOMElement=xml.dom.Element(xmlGraphElement.graphElementName)
newElement=xmlGraphElement(newDOMElement)
if name is not None:
newElement.setName(name)
return newElement
def createAlgorithmElement(self,name=None):
"""
creates a new algorithm element
"""
newDOMElement=None
if self.domElement.ownerDocument:
newDOMElement=self.domElement.ownerDocument.createElement(
xmlAlgorithmElement.algorithmElementName)
else:
newDOMElement=xml.dom.Element(xmlAlgorithmElement.algorithmElementName)
newElement=xmlAlgorithmElement(newDOMElement)
if name is not None:
newElement.setName(name)
return newElement
def setName(self,name):
"""
sets the name attribute for this element
"""
self.domElement.setAttribute("name",xmlEncode(name))
def getName(self,name):
"""
gets the name of this element, if unset an empty string
"""
return xmlDecode(self.domElement.getAttribute("name"))
def getAboutAsString(self):
requestedElements=filter(lambda e:e.nodeType==xml.dom.Node.ELEMENT_NODE and e.tagName=="about",
self.domElement.childNodes)
if len(requestedElements)<1:
return None
else:
return requestedElements[0].toxml()
def getGraphElements(self):
"""
returns all direct child elements with graph's tagName
"""
return filter(lambda x:(x.nodeType==xml.dom.minidom.Node.ELEMENT_NODE and
x.tagName==xmlGraphElement.graphElementName),
self.domElement.childNodes)
def getAlgorithmElements(self):
"""
returns all direct child elements with algorithm's tagName
"""
return filter(lambda x:(x.nodeType==xml.dom.minidom.Node.ELEMENT_NODE and
x.tagName==xmlAlgorithmElement.algorithmElementName),
self.domElement.childNodes)
def getGraphNames(self):
"""
extracts all graph names
"""
# return a list of name attribute values from graph elements
return map(lambda x:xmlDecode(x.getAttribute("name")),
self.getGraphElements())
def getAlgorithmNames(self):
"""
extracts all algorithm names
"""
# return a list of name attribute values from graph elements
return map(lambda x:xmlDecode(x.getAttribute("name")),
self.getAlgorithmElements())
def getGraphByName(self,name):
"""
gets a xmlGraphElement by its name
"""
graphs=filter(lambda x:x.getAttribute("name")==name,
self.getGraphElements())
return xmlGraphElement(graphs[0])
def getAlgorithmByName(self,name):
"""
gets a xmlAlgorithmElement by its name
"""
graphs=filter(lambda x:x.getAttribute("name")==name,
self.getAlgorithmElements())
return xmlAlgorithmElement(graphs[0])
def getDefaultSelection(self):
"""
searches for the default selection, e.g.
only one graph and one algorithm
"""
defaultSelection={}
graphs=self.getGraphNames()
algorithms=self.getAlgorithmNames()
if len(graphs)<2 and len(algorithms)<2:
if len(graphs)==1:
defaultSelection["graph"]=self.getGraphByName(graphs[0])
if len(algorithms)==1:
defaultSelection["algorithm"]=self.getAlgorithmByName(algorithms[0])
return defaultSelection
if self.domElement.hasAttribute("defaultGraph"):
defaultSelection["graph"]=self.getGraphByName(
self.domElement.getAttribute("defaultGraph"))
if self.domElement.hasAttribute("defaultAlgorithm"):
defaultSelection["algorithm"]=self.getAlgorithmByName(
self.domElement.getAttribute("defaultAlgorithm"))
return defaultSelection
def updateGraphByName(self,graph,name=None):
"""
appends or replaces a graph, if name not given, the graphs name is taken
"""
newGraph=None
newName=None
if issubclass(graph.__class__,Graph.Graph):
newGraph=xmlGraphElement(graph,name)
elif issubclass(graph.__class__,xmlGraphElement):
newGraph=graph
if name:
graph.setName(name)
else:
name=graph.getName()
else:
raise FileException("argument mismatch!")
graphs=filter(lambda x:x.getAttribute("name")==name,
self.getGraphElements())
if len(graphs):
# replace graph, more exactly the first one
self.domElement.replaceChild(newGraph.domElement,graphs[0])
else:
# append graph
self.domElement.appendChild(newGraph.domElement)
def updateAlgorithmByName(self,algorithm,name=None):
"""
appends or replaces an algorithm
"""
newAlgorithm=None
newName=None
if issubclass(algorithm.__class__,Gato.Algorithm):
newAlgorithm=xmlAlgorithmElement(algorithm,name)
elif issubclass(algorithm.__class__,xmlAlgorithmElement):
newAlgorithm=algorithm
if name:
algorithm.setName(name)
else:
name=algorithm.getName()
else:
raise FileException("argument mismatch!")
algorithms=filter(lambda x:x.getAttribute("name")==name,
self.getAlgorithmElements())
if len(algorithms):
# replace graph, more exactly the first one
self.domElement.replaceChild(newAlgorithm.domElement,graphs[0])
else:
# append graph
self.domElement.appendChild(newAlgorithm.domElement)
def removeGraphByName(self,name):
"""
removes the named graph
"""
graphs=filter(lambda x:x.getAttribute("name")==name,
self.getGraphElements())
map(self.domElement.removeChild,graphs)
def removeAlgorithmByName(self,name):
"""
removes the named algorithm
"""
algorithms=filter(lambda x:x.getAttribute("name")==name,
self.getAlgorithmElements())
map(self.domElement.removeChild,algorithms)
class ElementDisplay(Tkinter.Frame):
"""
displays the selected element
"""
def __init__(self,master,**config):
"""
"""
config.update({"width":500, "height":500})
Tkinter.Frame.__init__(self,master,bg="white",cnf=config)
self.pack_propagate(0) # keep everything in a rigid frame
self.displayedWidget=None
self.displayedNode=None
def clearDisplay(self):
"""
cleanup and display nothing
"""
# cleanup radically
for child in self.children.values():
child.pack_forget()
child.destroy()
self.displayedNode=None
def setDisplay(self,node):
"""
display node...
"""
if node is None:
self.clearDisplay()
return
else:
if self.displayedNode is not None and self.displayedNode==node:
return #nothing to do
self.clearDisplay()
displayedWidget=None
if node.tagName==xmlGraphElement.graphElementName:
html_data=xmlGraphElement(node).getAboutAsString()
displayedWidget=AboutWidget(self,html_data)
self.displayedNode=node
elif node.tagName==xmlAlgorithmElement.algorithmElementName:
displayedWidget=AlgorithmInfoWidget(self,xmlAlgorithmElement(node))
self.displayedNode=node
elif node.tagName==xmlGatoElement.gatoElementName:
html_data=xmlGatoElement(node).getAboutAsString()
displayedWidget=AboutWidget(self,html_data)
self.displayedNode=node
else:
print "toDo: ",node
if displayedWidget:
displayedWidget.pack(expand=1,fill=Tkinter.BOTH)
class AlgorithmInfoWidget(Tkinter.Frame):
"""
creates an algorithm widget
"""
def __init__(self,master,algorithmElement):
"""
create it
"""
Tkinter.Frame.__init__(self,master,highlightthickness=0)
html_data=algorithmElement.getAboutAsString()
self.About=AboutWidget(self,html_data)
self.About.pack(side=Tkinter.TOP,expand=1,fill=Tkinter.BOTH)
text_data=algorithmElement.getText()
self.Text=ScrolledText.ScrolledText(self,
background='white',
wrap='word',
font="Times 10")
self.Text.insert(Tkinter.END, text_data)
self.Text.pack(side=Tkinter.TOP,expand=1,fill=Tkinter.BOTH)
self.Text['state'] = Tkinter.DISABLED
class AboutWidget(ScrolledText.ScrolledText):
"""
displays html text
copied and modified from GatoDialogs.py
Basic class which provides a scrollable area for viewing HTML text
"""
def __init__(self, master, htmlcode):
ScrolledText.ScrolledText.__init__(self, master,
background='white',
wrap='word',
font="Times 10",
)
self.inserthtml(htmlcode)
def Update(self,htmlcode):
self.inserthtml(htmlcode)
def inserthtml(self, htmlcode):
if htmlcode is None:
htmlcode="""
No information avaliable !
"""
import formatter
import GatoDialogs
self['state'] = Tkinter.NORMAL
self.delete('0.0', Tkinter.END)
writer = GatoDialogs.HTMLWriter(self, self)
format = formatter.AbstractFormatter(writer)
parser = GatoDialogs.MyHTMLParser(format, self)
parser.feed(htmlcode)
parser.close()
self['state'] = Tkinter.DISABLED
class GatoFileButtonBar(Tkinter.Frame):
"""
holds the buttons for the node selection dialog
"""
def __init__(self, master, reporter, cnf={}, **config):
cnf.update(config)
Tkinter.Frame.__init__(self,master,cnf=cnf)
self.reporter=reporter
self.okButton=Tkinter.Button(self,text="Ok",command=lambda e=None :self.reporter("quit"))
self.okButton.grid(row=0,column=0,sticky=Tkinter.E+Tkinter.W)
self.cancelButton=Tkinter.Button(self,text="Cancel",command=lambda e=None:self.reporter("cancel"))
self.cancelButton.grid(row=0,column=1,sticky=Tkinter.E+Tkinter.W)
self.columnconfigure(0,weight=1)
self.columnconfigure(1,weight=1)
class GatoFileStructureWidget(TextTreeWidget.dom_structure_widget):
"""
customized for .gato files
"""
def __init__(self,master,dom,report_function,**conf):
"""
report function coordinates the element display
"""
TextTreeWidget.dom_structure_widget.__init__(self,master,dom,self.report_func,width=30,cnf=conf)
self.chosenNodes={}
self.report_to=report_function
def report_func(self,event,*opts):
"""
reports nodes that are marked aka chosen
"""
if event=="chosenNode":
# double click on node, or hit
if len(opts)<1 or opts[0] is None:
return
node=opts[0]
# is this a gato element?
if node.tagName==xmlGatoElement.gatoElementName:
if self.report_to and not self.report_to(event,node):
return
self.chosenNode(node,xmlGatoElement.gatoElementName)
# is it a valid graph or algorithm object?
if self.isGatoMemberElement(node)!=1:
# nothing to do...
return
if node.tagName==xmlAlgorithmElement.algorithmElementName:
if self.report_to and not self.report_to(event,node):
return
self.chosenNode(node,xmlAlgorithmElement.algorithmElementName)
elif node.tagName==xmlGraphElement.graphElementName:
if self.report_to and not self.report_to(event,node):
return
self.chosenNode(node,xmlGraphElement.graphElementName)
elif event=="selectedNode":
# selected Node
if len(opts)<1 or opts[0] is None:
return
node=opts[0]
if self.report_to:
self.report_to(event, node)
else:
print event, opts
def chosenNode(self,node,register):
"""
mark and save chosen node
"""
last=self.chosenNodes.get(register)
if last:
self.setNodeColor(last,"black")
if last==node:
# erase selection
self.chosenNodes[register]=None
else:
# new selection
self.chosenNodes[register]=node
self.setNodeColor(node,"red")
def nameNode(self,node):
"""
chose proper names for node
"""
if node.nodeType==xml.dom.Node.ELEMENT_NODE:
myName=xmlDecode(node.tagName)
if myName==xmlGatoElement.gatoElementName:
if node.hasAttribute("name"):
return "gato: %s"%xmlDecode(node.getAttribute("name"))
else:
return "gato"
elif myName==xmlGraphElement.graphElementName:
return "graph: %s"%xmlDecode(node.getAttribute("name"))
elif myName==xmlAlgorithmElement.algorithmElementName:
return "algorithm: %s"%xmlDecode(node.getAttribute("name"))
else:
return TextTreeWidget.dom_structure_widget.nameNode(self,node)
else:
return TextTreeWidget.dom_structure_widget.nameNode(self,node)
def isGatoMemberElement(self,node):
"""
if this is a subtag of gato, return actual depth
returns 0, if not in a gato element, or is a gato element itself
"""
parent=node.parentNode
if parent:
if parent.nodeType==xml.dom.Node.DOCUMENT_NODE:
return 0
elif parent.nodeType==xml.dom.Node.ELEMENT_NODE and parent.tagName=="gato":
return 1
else:
depth=self.isGatoMemberElement(parent)
if depth:
return depth+1
else:
return 0
else:
raise FileException("could not determine, if node is member of gato section")
def isVisible(self,node):
"""
filter out subsections of graph and algorithm in gato sections
"""
isMember=self.isGatoMemberElement(node)
if isMember==0:
return TextTreeWidget.dom_structure_widget.isVisible(self,node)
elif isMember>1:
# hide everything deeper than 1
return 0
else:
# hide everything except graph and algorithm
if (node.nodeType==xml.dom.Node.ELEMENT_NODE and
node.tagName in [xmlGraphElement.graphElementName,
xmlAlgorithmElement.algorithmElementName]):
return 1
else:
return 0
class GatoDOMDialog(Tkinter.Toplevel):
"""
contains the xml structure and the display of selected element and some buttons
opt_choose contains the elements, that may be chosen
mand_choose contains the elements, that must be choosen
"""
def __init__(self, master, dom, opt_choose=("algorithm","graph"), mand_choose=(), **config):
"""
create it
"""
self.dom=dom
self.opt_choose=opt_choose
self.mand_choose=mand_choose
self.result={} # None in case of canceled dialog, (graphNode, algoNode) in case of selection
Tkinter.Toplevel.__init__(self,master, cnf=config)
# the structure widget...
self.struct_widget=GatoFileStructureWidget(self, self.dom, self.reportFromStructure, font="Times 12")
self.struct_widget.grid(row=0, column=0, sticky=Tkinter.NSEW)
# and the element display....
self.display_widget=ElementDisplay(self,width=400)
self.display_widget.grid(row=0, column=1, sticky=Tkinter.NSEW)
# the button bar...
self.buttons=GatoFileButtonBar(self,self.reportFromButtons)
self.buttons.grid(row=1, column=0, columnspan=2, sticky=Tkinter.EW)
self.protocol("WM_DELETE_WINDOW", lambda e=None: self.reportFromButtons("cancel"))
def reportFromStructure(self,event,node):
"""
coordinates element display
"""
if event=="selectedNode":
self.display_widget.setDisplay(node)
elif event=="chosenNode":
# should be selected?
if node and node.nodeType==xml.dom.Node.ELEMENT_NODE and \
(node.tagName in self.opt_choose or node.tagName in self.mand_choose):
return 1
def reportFromButtons(self,event):
"""
gets interesting events from button bar
"""
if event=="quit":
# graph
graph=self.struct_widget.chosenNodes.get(xmlGraphElement.graphElementName)
if graph:
self.result[xmlGraphElement.graphElementName]=xmlGraphElement(graph)
else:
if xmlGraphElement.graphElementName in self.mand_choose:
return
# algorithm
algorithm=self.struct_widget.chosenNodes.get(xmlAlgorithmElement.algorithmElementName)
if algorithm:
self.result[xmlAlgorithmElement.algorithmElementName]=xmlAlgorithmElement(algorithm)
else:
if xmlAlgorithmElement.algorithmElementName in self.mand_choose:
return
# gato section
gato=self.struct_widget.chosenNodes.get(xmlGatoElement.gatoElementName)
if gato:
self.result[xmlGatoElement.gatoElementName]=xmlGatoElement(gato)
else:
if xmlGatoElement.gatoElementName in self.mand_choose:
return
self.destroy()
self.quit()
elif event=="cancel":
self.result=None
self.destroy()
self.quit()
else:
raise Exception("unknown event %s"%event)
def run(self):
"""
runs this dialog
"""
self.mainloop()
return self.result
class GatoFile:
"""
encapsulates the dom creation from file
"""
validModes=['r', # expect valid gato file and read this file
'w', # create new file or overwrite existing file
]
def __init__(self,myFile=None, mode='r'):
"""
open a file and read the dom from it, or provide an empty gato document
"""
if myFile:
if mode not in self.validModes:
raise FileException("mode %c not supported"%mode)
self.mode=mode
import types
# test if name or file is given
if self.mode=='r':
self.file=None
self.name=None
if type(myFile) in types.StringTypes:
if os.access(myFile,os.R_OK):
self.file=file(myFile,"r")
self.name=myFile
else:
urlRequest=urllib2.Request(myFile)
self.file=urllib2.urlopen(urlRequest,"r")
elif type(myFile)==types.FileType:
self.file=myFile
try:
# pull dom out of file
self.dom = xml.dom.minidom.parse(self.file)
except xml.dom.DOMException, e:
print e
raise FileException(str(e))
elif self.mode=='w':
if type(myFile) in types.StringTypes:
if os.access(myFile,os.W_OK):
self.file=file(myFile,"w")
self.name=myFile
else:
urlRequest=urllib2.Request(myFile)
self.file=urllib2.urlopen(urlRequest,"w")
else:
self.file=myFile
# create empty document
self.dom = xml.dom.minidom.Document()
else: #myFile=None
# without file, but create empty doc, too
self.dom = xml.dom.minidom.Document()
# self.dom.appendChild(self.dom.createElement(xmlGatoElement.gatoElementName))
def write(self,_file=None):
"""
writes the (modified) dom to the file
"""
# ???? write to right place????
_thisFile=None
if _file is not None:
if type(_file) in types.StringTypes:
_thisFile=encodedStream(file(_file,"w"))
elif type(_file)==types.FileType:
_thisFile=_file
else:
_thisFile=self.file
encoding="iso-8859-1"
_thisFile.write("\n"%encoding)
if self.dom.hasChildNodes():
for n in self.dom.childNodes:
n.writexml(xmlEncodedStream(_thisFile))
def getDefaultSelection(self):
"""
returns the default selected elements
"""
if (not self.dom.documentElement) or \
self.dom.documentElement.tagName!=xmlGatoElement.gatoElementName:
return None
else:
return xmlGatoElement(self.dom.documentElement).getDefaultSelection()
def getGatoElements(self):
"""
returns an xmlGatoElement instance
"""
return self.dom.getElementsByTagName(xmlGatoElement.gatoElementName)
def appendGatoElement(self,newElement):
"""
append the element without checking unique name
"""
if self.dom.hasChildNodes():
# replace doc root gato node with a more general section
if self.dom.documentElement.tagName==xmlGatoElement.gatoElementName:
tmpGatoElement=self.dom.documentElement
self.dom.replaceChild(self.dom.createElement("xml"),tmpGatoElement)
self.dom.documentElement.appendChild(tmpGatoElement)
# append to document element
self.dom.documentElement.appendChild(newElement.domElement)
else:
# start with a gato node as root element
self.dom.appendChild(newElement.domElement)
def createGatoElement(self,name=None):
"""
creates a new (opt. unnamed) Gato element
"""
newElement=xmlGatoElement(self.dom.createElement(xmlGatoElement.gatoElementName))
if name is not None:
newElement.setName(name)
return newElement
def displaySelectionDialog(self,master):
"""
runs the dom structure
"""
w=GatoDOMDialog(master,self.dom)
return w.run()
def displayGraphSelectionDialog(self,master):
w=GatoDOMDialog(master,self.dom,to_choose=(('graph',)))
s=w.run()
if s:
return s["graph"]
else:
return None
def displayAlgorithmSelectionDialog(self,master):
w=GatoDOMDialog(master, self.dom,to_choose=(('algorithm',)))
s=w.run()
if s:
return s["algorithm"]
else:
return None
if __name__=='__main__':
# some test progs
WorkInProgress().run()
# old ones
# f=GatoFile('DFS.gato') # parse an XML file by name
# f.displayGraphSelectionDialog(Tkinter.Tk())
lif myName==xmlAlgorithmElement.algorithmElementName:
return "algorithm: %s"%xmlDecode(node.getAttribute("name"))
else:
return TextTreeWidget.dom_structure_widget.nameNode(self,node)
else:
return TextTreeWidget.dom_structure_widget.nameNode(self,node)
def isGatoMemberEleGato/GatoGlobals.py 0100644 0017676 0017534 00000070770 10014153351 0015574 0 ustar 00schliep catbox 0000305 0050066 ################################################################################
#
# This file is part of Gato (Graph Animation Toolbox)
#
# file: GatoGlobals.py
# author: Alexander Schliep (schliep@molgen.mpg.de)
#
# Copyright (C) 1998-2003, Alexander Schliep, Winfried Hochstaettler and
# ZAIK/ZPR, Universitaet zu Koeln
#
# Contact: schliep@molgen.mpg.de, wh@zpr.uni-koeln.de
#
# Information: http://gato.sf.net
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Library General Public
# License as published by the Free Software Foundation; either
# version 2 of the License, or (at your option) any later version.
#
# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Library General Public License for more details.
#
# You should have received a copy of the GNU Library General Public
# License along with this library; if not, write to the Free
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#
#
#
# This file is version $Revision: 1.18 $
# from $Date: 2003/06/03 20:09:50 $
# last change by $Author: schliep $.
#
#
################################################################################
# Globals
### This will be set in GraphDisplay.ReadConfiguration()
##gVertexRadius = 12
##gVertexFrameWidth = 2
##gEdgeWidth = 3
# Animation
gBlinkRate = 10 # ms 100
gBlinkRepeat = 3 # One more than you want 4
# Printing
gPaperHeight = 1500 # "20i" XXX Should be real paper size
gPaperWidth = 1500 # "20i"
gInteractive = 1
gGridSize = 50 # Grid size for editor
gInfinity = 9999999
True = 1
False = 0
### Internal Color Names for GraphDisplay and such
### This will be set in GraphDisplay.ReadConfiguration()
##cVertexDefault = "red"
##cVertexBlink = "black"
##cEdgeDefault = "black"
##cLabelDefault = "black"
##cLabelDefaultInverted = "white"
##cLabelBlink = "green"
cInitial = "green"
cVisited = "grey"
#cOnQueue = "red"
#cRemovedFromQueue = "blue"
cOnQueue = "blue"
cRemovedFromQueue = "grey"
cTraversedEdge = "grey"
gColors = ['#333333','#663333','#993333','#CC3333',
'#336633','#666633','#996633','#CC6633',
'#339933','#669933','#999933','#CC9933',
'#33CC33','#66CC33','#99CC33','#CCCC33',
'#333366','#663366','#993366','#CC3366',
'#336666','#666666','#996666','#CC6666',
'#339966','#669966','#999966','#CC9966',
'#33CC66','#66CC66','#99CC66','#CCCC66',
'#3333CC','#6633CC','#9933CC','#CC33CC',
'#3366CC','#6666CC','#9966CC','#CC66CC',
'#3399CC','#6699CC','#9999CC','#CC99CC',
'#33CCCC','#66CCCC','#99CCCC','#CCCCCC']
# Exceptions
GraphNotSimpleError = 'GraphNotSimpleError'
NoSuchVertexError = 'NoSuchVertexError'
NoSuchEdgeError = 'NoSuchEdgeError'
gProperty = {}
# property explanation
#gProperty = {'Connected': 'has more than one connected component'} #,
# 'Directed': 'edges are not directed',
# 'EdgeWeights': 'not enough weights on the edges',
# 'VertexWeights': 'not enough weights on the vertices'
# }
gGatoHelp = """
Gato 0.98J
Keyboard Shortcuts
- s Start
- x Stop
- [space] Step
- c Continue
- t trace
- b toggle breakpoint
"""
gLGPLText = """
GNU LIBRARY GENERAL PUBLIC LICENSE
Version 2, June 1991
Copyright (C) 1991 Free Software Foundation, Inc.
59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
[This is the first released version of the library GPL. It is
numbered 2 because it goes with version 2 of the ordinary GPL.]
Preamble
The licenses for most software are designed to take away your
freedom to share and change it. By contrast, the GNU General Public
Licenses are intended to guarantee your freedom to share and change
free software--to make sure the software is free for all its users.
This license, the Library General Public License, applies to some
specially designated Free Software Foundation software, and to any
other libraries whose authors decide to use it. You can use it for
your libraries, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
this service if you wish), that you receive source code or can get it
if you want it, that you can change the software or use pieces of it
in new free programs; and that you know you can do these things.
To protect your rights, we need to make restrictions that forbid
anyone to deny you these rights or to ask you to surrender the rights.
These restrictions translate to certain responsibilities for you if
you distribute copies of the library, or if you modify it.
For example, if you distribute copies of the library, whether gratis
or for a fee, you must give the recipients all the rights that we gave
you. You must make sure that they, too, receive or can get the source
code. If you link a program with the library, you must provide
complete object files to the recipients so that they can relink them
with the library, after making changes to the library and recompiling
it. And you must show them these terms so they know their rights.
Our method of protecting your rights has two steps: (1) copyright
the library, and (2) offer you this license which gives you legal
permission to copy, distribute and/or modify the library.
Also, for each distributor's protection, we want to make certain
that everyone understands that there is no warranty for this free
library. If the library is modified by someone else and passed on, we
want its recipients to know that what they have is not the original
version, so that any problems introduced by others will not reflect on
the original authors' reputations.
Finally, any free program is threatened constantly by software
patents. We wish to avoid the danger that companies distributing free
software will individually obtain patent licenses, thus in effect
transforming the program into proprietary software. To prevent this,
we have made it clear that any patent must be licensed for everyone's
free use or not licensed at all.
Most GNU software, including some libraries, is covered by the ordinary
GNU General Public License, which was designed for utility programs. This
license, the GNU Library General Public License, applies to certain
designated libraries. This license is quite different from the ordinary
one; be sure to read it in full, and don't assume that anything in it is
the same as in the ordinary license.
The reason we have a separate public license for some libraries is that
they blur the distinction we usually make between modifying or adding to a
program and simply using it. Linking a program with a library, without
changing the library, is in some sense simply using the library, and is
analogous to running a utility program or application program. However, in
a textual and legal sense, the linked executable is a combined work, a
derivative of the original library, and the ordinary General Public License
treats it as such.
Because of this blurred distinction, using the ordinary General
Public License for libraries did not effectively promote software
sharing, because most developers did not use the libraries. We
concluded that weaker conditions might promote sharing better.
However, unrestricted linking of non-free programs would deprive the
users of those programs of all benefit from the free status of the
libraries themselves. This Library General Public License is intended to
permit developers of non-free programs to use free libraries, while
preserving your freedom as a user of such programs to change the free
libraries that are incorporated in them. (We have not seen how to achieve
this as regards changes in header files, but we have achieved it as regards
changes in the actual functions of the Library.) The hope is that this
will lead to faster development of free libraries.
The precise terms and conditions for copying, distribution and
modification follow. Pay close attention to the difference between a
\"work based on the library\" and a \"work that uses the library\". The
former contains code derived from the library, while the latter only
works together with the library.
Note that it is possible for a library to be covered by the ordinary
General Public License rather than by this special one.
GNU LIBRARY GENERAL PUBLIC LICENSE
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
0. This License Agreement applies to any software library which
contains a notice placed by the copyright holder or other authorized
party saying it may be distributed under the terms of this Library
General Public License (also called \"this License\"). Each licensee is
addressed as \"you\".
A \"library\" means a collection of software functions and/or data
prepared so as to be conveniently linked with application programs
(which use some of those functions and data) to form executables.
The \"Library\", below, refers to any such software library or work
which has been distributed under these terms. A \"work based on the
Library\" means either the Library or any derivative work under
copyright law: that is to say, a work containing the Library or a
portion of it, either verbatim or with modifications and/or translated
straightforwardly into another language. (Hereinafter, translation is
included without limitation in the term \"modification\".)
\"Source code\" for a work means the preferred form of the work for
making modifications to it. For a library, complete source code means
all the source code for all modules it contains, plus any associated
interface definition files, plus the scripts used to control compilation
and installation of the library.
Activities other than copying, distribution and modification are not
covered by this License; they are outside its scope. The act of
running a program using the Library is not restricted, and output from
such a program is covered only if its contents constitute a work based
on the Library (independent of the use of the Library in a tool for
writing it). Whether that is true depends on what the Library does
and what the program that uses the Library does.
1. You may copy and distribute verbatim copies of the Library's
complete source code as you receive it, in any medium, provided that
you conspicuously and appropriately publish on each copy an
appropriate copyright notice and disclaimer of warranty; keep intact
all the notices that refer to this License and to the absence of any
warranty; and distribute a copy of this License along with the
Library.
You may charge a fee for the physical act of transferring a copy,
and you may at your option offer warranty protection in exchange for a
fee.
2. You may modify your copy or copies of the Library or any portion
of it, thus forming a work based on the Library, and copy and
distribute such modifications or work under the terms of Section 1
above, provided that you also meet all of these conditions:
a) The modified work must itself be a software library.
b) You must cause the files modified to carry prominent notices
stating that you changed the files and the date of any change.
c) You must cause the whole of the work to be licensed at no
charge to all third parties under the terms of this License.
d) If a facility in the modified Library refers to a function or a
table of data to be supplied by an application program that uses
the facility, other than as an argument passed when the facility
is invoked, then you must make a good faith effort to ensure that,
in the event an application does not supply such function or
table, the facility still operates, and performs whatever part of
its purpose remains meaningful.
(For example, a function in a library to compute square roots has
a purpose that is entirely well-defined independent of the
application. Therefore, Subsection 2d requires that any
application-supplied function or table used by this function must
be optional: if the application does not supply it, the square
root function must still compute square roots.)
These requirements apply to the modified work as a whole. If
identifiable sections of that work are not derived from the Library,
and can be reasonably considered independent and separate works in
themselves, then this License, and its terms, do not apply to those
sections when you distribute them as separate works. But when you
distribute the same sections as part of a whole which is a work based
on the Library, the distribution of the whole must be on the terms of
this License, whose permissions for other licensees extend to the
entire whole, and thus to each and every part regardless of who wrote
it.
Thus, it is not the intent of this section to claim rights or contest
your rights to work written entirely by you; rather, the intent is to
exercise the right to control the distribution of derivative or
collective works based on the Library.
In addition, mere aggregation of another work not based on the Library
with the Library (or with a work based on the Library) on a volume of
a storage or distribution medium does not bring the other work under
the scope of this License.
3. You may opt to apply the terms of the ordinary GNU General Public
License instead of this License to a given copy of the Library. To do
this, you must alter all the notices that refer to this License, so
that they refer to the ordinary GNU General Public License, version 2,
instead of to this License. (If a newer version than version 2 of the
ordinary GNU General Public License has appeared, then you can specify
that version instead if you wish.) Do not make any other change in
these notices.
Once this change is made in a given copy, it is irreversible for
that copy, so the ordinary GNU General Public License applies to all
subsequent copies and derivative works made from that copy.
This option is useful when you wish to copy part of the code of
the Library into a program that is not a library.
4. You may copy and distribute the Library (or a portion or
derivative of it, under Section 2) in object code or executable form
under the terms of Sections 1 and 2 above provided that you accompany
it with the complete corresponding machine-readable source code, which
must be distributed under the terms of Sections 1 and 2 above on a
medium customarily used for software interchange.
If distribution of object code is made by offering access to copy
from a designated place, then offering equivalent access to copy the
source code from the same place satisfies the requirement to
distribute the source code, even though third parties are not
compelled to copy the source along with the object code.
5. A program that contains no derivative of any portion of the
Library, but is designed to work with the Library by being compiled or
linked with it, is called a \"work that uses the Library\". Such a
work, in isolation, is not a derivative work of the Library, and
therefore falls outside the scope of this License.
However, linking a \"work that uses the Library\" with the Library
creates an executable that is a derivative of the Library (because it
contains portions of the Library), rather than a \"work that uses the
library\". The executable is therefore covered by this License.
Section 6 states terms for distribution of such executables.
When a \"work that uses the Library\" uses material from a header file
that is part of the Library, the object code for the work may be a
derivative work of the Library even though the source code is not.
Whether this is true is especially significant if the work can be
linked without the Library, or if the work is itself a library. The
threshold for this to be true is not precisely defined by law.
If such an object file uses only numerical parameters, data
structure layouts and accessors, and small macros and small inline
functions (ten lines or less in length), then the use of the object
file is unrestricted, regardless of whether it is legally a derivative
work. (Executables containing this object code plus portions of the
Library will still fall under Section 6.)
Otherwise, if the work is a derivative of the Library, you may
distribute the object code for the work under the terms of Section 6.
Any executables containing that work also fall under Section 6,
whether or not they are linked directly with the Library itself.
6. As an exception to the Sections above, you may also compile or
link a \"work that uses the Library\" with the Library to produce a
work containing portions of the Library, and distribute that work
under terms of your choice, provided that the terms permit
modification of the work for the customer's own use and reverse
engineering for debugging such modifications.
You must give prominent notice with each copy of the work that the
Library is used in it and that the Library and its use are covered by
this License. You must supply a copy of this License. If the work
during execution displays copyright notices, you must include the
copyright notice for the Library among them, as well as a reference
directing the user to the copy of this License. Also, you must do one
of these things:
a) Accompany the work with the complete corresponding
machine-readable source code for the Library including whatever
changes were used in the work (which must be distributed under
Sections 1 and 2 above); and, if the work is an executable linked
with the Library, with the complete machine-readable \"work that
uses the Library\", as object code and/or source code, so that the
user can modify the Library and then relink to produce a modified
executable containing the modified Library. (It is understood
that the user who changes the contents of definitions files in the
Library will not necessarily be able to recompile the application
to use the modified definitions.)
b) Accompany the work with a written offer, valid for at
least three years, to give the same user the materials
specified in Subsection 6a, above, for a charge no more
than the cost of performing this distribution.
c) If distribution of the work is made by offering access to copy
from a designated place, offer equivalent access to copy the above
specified materials from the same place.
d) Verify that the user has already received a copy of these
materials or that you have already sent this user a copy.
For an executable, the required form of the \"work that uses the
Library\" must include any data and utility programs needed for
reproducing the executable from it. However, as a special exception,
the source code distributed need not include anything that is normally
distributed (in either source or binary form) with the major
components (compiler, kernel, and so on) of the operating system on
which the executable runs, unless that component itself accompanies
the executable.
It may happen that this requirement contradicts the license
restrictions of other proprietary libraries that do not normally
accompany the operating system. Such a contradiction means you cannot
use both them and the Library together in an executable that you
distribute.
7. You may place library facilities that are a work based on the
Library side-by-side in a single library together with other library
facilities not covered by this License, and distribute such a combined
library, provided that the separate distribution of the work based on
the Library and of the other library facilities is otherwise
permitted, and provided that you do these two things:
a) Accompany the combined library with a copy of the same work
based on the Library, uncombined with any other library
facilities. This must be distributed under the terms of the
Sections above.
b) Give prominent notice with the combined library of the fact
that part of it is a work based on the Library, and explaining
where to find the accompanying uncombined form of the same work.
8. You may not copy, modify, sublicense, link with, or distribute
the Library except as expressly provided under this License. Any
attempt otherwise to copy, modify, sublicense, link with, or
distribute the Library is void, and will automatically terminate your
rights under this License. However, parties who have received copies,
or rights, from you under this License will not have their licenses
terminated so long as such parties remain in full compliance.
9. You are not required to accept this License, since you have not
signed it. However, nothing else grants you permission to modify or
distribute the Library or its derivative works. These actions are
prohibited by law if you do not accept this License. Therefore, by
modifying or distributing the Library (or any work based on the
Library), you indicate your acceptance of this License to do so, and
all its terms and conditions for copying, distributing or modifying
the Library or works based on it.
10. Each time you redistribute the Library (or any work based on the
Library), the recipient automatically receives a license from the
original licensor to copy, distribute, link with or modify the Library
subject to these terms and conditions. You may not impose any further
restrictions on the recipients' exercise of the rights granted herein.
You are not responsible for enforcing compliance by third parties to
this License.
11. If, as a consequence of a court judgment or allegation of patent
infringement or for any other reason (not limited to patent issues),
conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot
distribute so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you
may not distribute the Library at all. For example, if a patent
license would not permit royalty-free redistribution of the Library by
all those who receive copies directly or indirectly through you, then
the only way you could satisfy both it and this License would be to
refrain entirely from distribution of the Library.
If any portion of this section is held invalid or unenforceable under any
particular circumstance, the balance of the section is intended to apply,
and the section as a whole is intended to apply in other circumstances.
It is not the purpose of this section to induce you to infringe any
patents or other property right claims or to contest validity of any
such claims; this section has the sole purpose of protecting the
integrity of the free software distribution system which is
implemented by public license practices. Many people have made
generous contributions to the wide range of software distributed
through that system in reliance on consistent application of that
system; it is up to the author/donor to decide if he or she is willing
to distribute software through any other system and a licensee cannot
impose that choice.
This section is intended to make thoroughly clear what is believed to
be a consequence of the rest of this License.
12. If the distribution and/or use of the Library is restricted in
certain countries either by patents or by copyrighted interfaces, the
original copyright holder who places the Library under this License may add
an explicit geographical distribution limitation excluding those countries,
so that distribution is permitted only in or among countries not thus
excluded. In such case, this License incorporates the limitation as if
written in the body of this License.
13. The Free Software Foundation may publish revised and/or new
versions of the Library General Public License from time to time.
Such new versions will be similar in spirit to the present version,
but may differ in detail to address new problems or concerns.
Each version is given a distinguishing version number. If the Library
specifies a version number of this License which applies to it and
\"any later version\", you have the option of following the terms and
conditions either of that version or of any later version published by
the Free Software Foundation. If the Library does not specify a
license version number, you may choose any version ever published by
the Free Software Foundation.
14. If you wish to incorporate parts of the Library into other free
programs whose distribution conditions are incompatible with these,
write to the author to ask for permission. For software which is
copyrighted by the Free Software Foundation, write to the Free
Software Foundation; we sometimes make exceptions for this. Our
decision will be guided by the two goals of preserving the free status
of all derivatives of our free software and of promoting the sharing
and reuse of software generally.
NO WARRANTY
15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO
WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW.
EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR
OTHER PARTIES PROVIDE THE LIBRARY \"AS IS\" WITHOUT WARRANTY OF ANY
KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE
LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME
THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN
WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY
AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU
FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR
CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE
LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING
RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A
FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF
SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
DAMAGES.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Libraries
If you develop a new library, and you want it to be of the greatest
possible use to the public, we recommend making it free software that
everyone can redistribute and change. You can do so by permitting
redistribution under these terms (or, alternatively, under the terms of the
ordinary General Public License).
To apply these terms, attach the following notices to the library. It is
safest to attach them to the start of each source file to most effectively
convey the exclusion of warranty; and each file should have at least the
\"copyright\" line and a pointer to where the full notice is found.
Copyright (C)
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Library General Public
License as published by the Free Software Foundation; either
version 2 of the License, or (at your option) any later version.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Library General Public License for more details.
You should have received a copy of the GNU Library General Public
License along with this library; if not, write to the Free
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
Also add information on how to contact you by electronic and paper mail.
You should also get your employer (if you work as a programmer) or your
school, if any, to sign a \"copyright disclaimer\" for the library, if
necessary. Here is a sample; alter the names:
Yoyodyne, Inc., hereby disclaims all copyright interest in the
library `Frob' (a library for tweaking knobs) written by James Random Hacker.
, 1 April 1990
Ty Coon, President of Vice
That's all there is to it!
"""
leaGato/GatoIcons.py 0100644 0017676 0017534 00000302132 10014153352 0015253 0 ustar 00schliep catbox 0000305 0050066 ################################################################################
#
# This file is part of Gato (Graph Animation Toolbox)
#
# file: GatoIcons.py
#
# NOTE: Automatically created by mkGatoIcons.pl
# Do *not* edit this file manually
#
#
#
# Copyright (C) 1998-2003, Alexander Schliep, Winfried Hochstaettler and
# ZAIK/ZPR, Universitaet zu Koeln
#
# Contact: schliep@molgen.mpg.de, wh@zpr.uni-koeln.de
#
# Information: http://gato.sf.net
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Library General Public
# License as published by the Free Software Foundation; either
# version 2 of the License, or (at your option) any later version.
#
# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Library General Public License for more details.
#
# You should have received a copy of the GNU Library General Public
# License along with this library; if not, write to the Free
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#
#
#
#
################################################################################
black = """
R0lGODdhHgAeAPcAAAAAAAAAAAAAMwAAZgAAmQAAzAAA/wAzAAAzMwAzZgAzmQAzzAAz/wBmAABm
MwBmZgBmmQBmzABm/wCZAACZMwCZZgCZmQCZzACZ/wDMAADMMwDMZgDMmQDMzADM/wD/AAD/MwD/
ZgD/mQD/zAD//zMAADMAMzMAZjMAmTMAzDMA/zMzADMzMzMzZjMzmTMzzDMz/zNmADNmMzNmZjNm
mTNmzDNm/zOZADOZMzOZZjOZmTOZzDOZ/zPMADPMMzPMZjPMmTPMzDPM/zP/ADP/MzP/ZjP/mTP/
zDP//2YAAGYAM2YAZmYAmWYAzGYA/2YzAGYzM2YzZmYzmWYzzGYz/2ZmAGZmM2ZmZmZmmWZmzGZm
/2aZAGaZM2aZZmaZmWaZzGaZ/2bMAGbMM2bMZmbMmWbMzGbM/2b/AGb/M2b/Zmb/mWb/zGb//5kA
AJkAM5kAZpkAmZkAzJkA/5kzAJkzM5kzZpkzmZkzzJkz/5lmAJlmM5lmZplmmZlmzJlm/5mZAJmZ
M5mZZpmZmZmZzJmZ/5nMAJnMM5nMZpnMmZnMzJnM/5n/AJn/M5n/Zpn/mZn/zJn//8wAAMwAM8wA
ZswAmcwAzMwA/8wzAMwzM8wzZswzmcwzzMwz/8xmAMxmM8xmZsxmmcxmzMxm/8yZAMyZM8yZZsyZ
mcyZzMyZ/8zMAMzMM8zMZszMmczMzMzM/8z/AMz/M8z/Zsz/mcz/zMz///8AAP8AM/8AZv8Amf8A
zP8A//8zAP8zM/8zZv8zmf8zzP8z//9mAP9mM/9mZv9mmf9mzP9m//+ZAP+ZM/+ZZv+Zmf+ZzP+Z
///MAP/MM//MZv/Mmf/MzP/M////AP//M///Zv//mf//zP///wAAAA0NDRoaGigoKDU1NUNDQ1BQ
UF1dXWtra3h4eIaGhpOTk6Ghoa6urru7u8nJydbW1uTk5PHx8f///wAAAAEBAQICAgMDAwQEBAUF
BQYGBgcHBwgICAkJCQoKCgsLCwwMDA0NDQ4ODg8PDxAQEBERERISEiwAAAAAHgAeAAcIMgABCBxI
sKDBgwgTKlzIsKHDhxAjSpxIsaLFixgzatzIsaPHjyBDihxJsqTJkyhTngwoADs=
"""
blacknde = """
R0lGODdhGgAYALMAAAAAAIAAAACAAICAAAAAgIAAgACAgICAgMDAwP8AAAD/AP//AAAA//8A/wD/
/////ywAAAAAGgAYAAMEgPDJSau9eILMJTAfEIBbNxmBCKzsaD5sOKZtianHSoM5yYUs0IOWmt2I
KA1paaOESCxYUbe6qGg9GIgZsDJ1UmrKYKFeP9rVtlpeA8NTkXVKTDPJbfW7dp2LATk5T1FeQDFn
aDcqHyMiUEk/gIdBNCYsOTMobC8wk5ucoKGioxMRADs=
"""
bledge = """
R0lGODdhHgAeAPcAAACZzAAAAAAAMwAAZgAAmQAAzAAA/wAzAAAzMwAzZgAzmQAzzAAz/wBmAABm
MwBmZgBmmQBmzABm/wCZAACZMwCZZgCZmQCZzACZ/wDMAADMMwDMZgDMmQDMzADM/wD/AAD/MwD/
ZgD/mQD/zAD//zMAADMAMzMAZjMAmTMAzDMA/zMzADMzMzMzZjMzmTMzzDMz/zNmADNmMzNmZjNm
mTNmzDNm/zOZADOZMzOZZjOZmTOZzDOZ/zPMADPMMzPMZjPMmTPMzDPM/zP/ADP/MzP/ZjP/mTP/
zDP//2YAAGYAM2YAZmYAmWYAzGYA/2YzAGYzM2YzZmYzmWYzzGYz/2ZmAGZmM2ZmZmZmmWZmzGZm
/2aZAGaZM2aZZmaZmWaZzGaZ/2bMAGbMM2bMZmbMmWbMzGbM/2b/AGb/M2b/Zmb/mWb/zGb//5kA
AJkAM5kAZpkAmZkAzJkA/5kzAJkzM5kzZpkzmZkzzJkz/5lmAJlmM5lmZplmmZlmzJlm/5mZAJmZ
M5mZZpmZmZmZzJmZ/5nMAJnMM5nMZpnMmZnMzJnM/5n/AJn/M5n/Zpn/mZn/zJn//8wAAMwAM8wA
ZswAmcwAzMwA/8wzAMwzM8wzZswzmcwzzMwz/8xmAMxmM8xmZsxmmcxmzMxm/8yZAMyZM8yZZsyZ
mcyZzMyZ/8zMAMzMM8zMZszMmczMzMzM/8z/AMz/M8z/Zsz/mcz/zMz///8AAP8AM/8AZv8Amf8A
zP8A//8zAP8zM/8zZv8zmf8zzP8z//9mAP9mM/9mZv9mmf9mzP9m//+ZAP+ZM/+ZZv+Zmf+ZzP+Z
///MAP/MM//MZv/Mmf/MzP/M////AP//M///Zv//mf//zP///wAAAA0NDRoaGigoKDU1NUNDQ1BQ
UF1dXWtra3h4eIaGhpOTk6Ghoa6urru7u8nJydbW1uTk5PHx8f///wAAAAEBAQICAgMDAwQEBAUF
BQYGBgcHBwgICAkJCQoKCgsLCwwMDA0NDQ4ODg8PDxAQEBERERISEiwAAAAAHgAeAAcIMgABCBxI
sKDBgwgTKlzIsKHDhxAjSpxIsaLFixgzatzIsaPHjyBDihxJsqTJkyhTngwoADs=
"""
bluenode = """
R0lGODdhGgAYALMAAAAAAIAAAACAAICAAAAAgIAAgACAgICAgMDAwP8AAAD/AP//AAAA//8A/wD/
/////ywAAAAAGgAYAAMEWPDJSau9eILMJfgg2FHhx5TjEzJse4oZ6M7sx5kzQNuX3PouWAWXo716
up2RN0wWd5um8bSMSpVQ5LKqxeasJOK0xryOyTEx9wYMCtMlVMoTL8/v+LxeEiEAOw==
"""
catbox_splash = """
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
"""
delete = """
R0lGODdhIAAgAPcPAAAAAIAAAACAAICAAAAAgIAAgACAgMDAwICAgP8AAAD/AP//AAAA//8A/wD/
/////wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAEAAAEALAAAAAAgACAA
BwiTAAMIHEiwoMGDCBMqXMiwocOHECNKNAigosWLFiUCOMCxo8cDFSNu/PhxIwCII0l6NPkwpUqO
I082dPkypcyFNFW6vJkwp86OPBH6JLkT58uSK40eBZpU4VCiTYUuXRn1IMarGEs6ncoUqFKuNhk+
RQpTLFeYZc2eZTnzLEiQDseuDLp1Ld2eWPNO3Mu3r9+/CwMCADs=
"""
delete_1 = """
R0lGODlhGwAbAPcAAAAAAAAAMwAAZgAAmQAAzAAA/wAzAAAzMwAzZgAzmQAzzAAz/wBmAABmMwBm
ZgBmmQBmzABm/wCZAACZMwCZZgCZmQCZzACZ/wDMAADMMwDMZgDMmQDMzADM/wD/AAD/MwD/ZgD/
mQD/zAD//zMAADMAMzMAZjMAmTMAzDMA/zMzADMzMzMzZjMzmTMzzDMz/zNmADNmMzNmZjNmmTNm
zDNm/zOZADOZMzOZZjOZmTOZzDOZ/zPMADPMMzPMZjPMmTPMzDPM/zP/ADP/MzP/ZjP/mTP/zDP/
/2YAAGYAM2YAZmYAmWYAzGYA/2YzAGYzM2YzZmYzmWYzzGYz/2ZmAGZmM2ZmZmZmmWZmzGZm/2aZ
AGaZM2aZZmaZmWaZzGaZ/2bMAGbMM2bMZmbMmWbMzGbM/2b/AGb/M2b/Zmb/mWb/zGb//5kAAJkA
M5kAZpkAmZkAzJkA/5kzAJkzM5kzZpkzmZkzzJkz/5lmAJlmM5lmZplmmZlmzJlm/5mZAJmZM5mZ
ZpmZmZmZzJmZ/5nMAJnMM5nMZpnMmZnMzJnM/5n/AJn/M5n/Zpn/mZn/zJn//8wAAMwAM8wAZswA
mcwAzMwA/8wzAMwzM8wzZswzmcwzzMwz/8xmAMxmM8xmZsxmmcxmzMxm/8yZAMyZM8yZZsyZmcyZ
zMyZ/8zMAMzMM8zMZszMmczMzMzM/8z/AMz/M8z/Zsz/mcz/zMz///8AAP8AM/8AZv8Amf8AzP8A
//8zAP8zM/8zZv8zmf8zzP8z//9mAP9mM/9mZv9mmf9mzP9m//+ZAP+ZM/+ZZv+Zmf+ZzP+Z///M
AP/MM//MZv/Mmf/MzP/M////AP//M///Zv//mf//zP///wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAkMAB4ALAAAAAAbABsA
AAjUAD0IHEiw4MAVCFcYXMjQQ8KEDSN6CEQx0EOJCytWvIhxoEaNHDF+DARgI0SJFUuS/Hiy4bWX
KlWCRBjx5TUA11ayXGHRJUyYIxEGsqJwoU2cN1/OHLrCilGbP21SFErUqUGoOZFCpcrz6TVWrCxi
/WqFqdWCSjWODctKqNecYANhZVt1ISuoH23S7WoQ7F2lawOxqtv3K9ixL/cGYnjYb+DBTRsKvuv3
71fBRBdHtNK4shXOhFF2Hgyab0fOlD9DPttx4uHSmlsP/Ew7suyCZd3KDggAOw==
"""
delete_2 = """
R0lGODlhGwAbAPcAAAAAAAAAMwAAZgAAmQAAzAAA/wAzAAAzMwAzZgAzmQAzzAAz/wBmAABmMwBm
ZgBmmQBmzABm/wCZAACZMwCZZgCZmQCZzACZ/wDMAADMMwDMZgDMmQDMzADM/wD/AAD/MwD/ZgD/
mQD/zAD//zMAADMAMzMAZjMAmTMAzDMA/zMzADMzMzMzZjMzmTMzzDMz/zNmADNmMzNmZjNmmTNm
zDNm/zOZADOZMzOZZjOZmTOZzDOZ/zPMADPMMzPMZjPMmTPMzDPM/zP/ADP/MzP/ZjP/mTP/zDP/
/2YAAGYAM2YAZmYAmWYAzGYA/2YzAGYzM2YzZmYzmWYzzGYz/2ZmAGZmM2ZmZmZmmWZmzGZm/2aZ
AGaZM2aZZmaZmWaZzGaZ/2bMAGbMM2bMZmbMmWbMzGbM/2b/AGb/M2b/Zmb/mWb/zGb//5kAAJkA
M5kAZpkAmZkAzJkA/5kzAJkzM5kzZpkzmZkzzJkz/5lmAJlmM5lmZplmmZlmzJlm/5mZAJmZM5mZ
ZpmZmZmZzJmZ/5nMAJnMM5nMZpnMmZnMzJnM/5n/AJn/M5n/Zpn/mZn/zJn//8wAAMwAM8wAZswA
mcwAzMwA/8wzAMwzM8wzZswzmcwzzMwz/8xmAMxmM8xmZsxmmcxmzMxm/8yZAMyZM8yZZsyZmcyZ
zMyZ/8zMAMzMM8zMZszMmczMzMzM/8z/AMz/M8z/Zsz/mcz/zMz///8AAP8AM/8AZv8Amf8AzP8A
//8zAP8zM/8zZv8zmf8zzP8z//9mAP9mM/9mZv9mmf9mzP9m//+ZAP+ZM/+ZZv+Zmf+ZzP+Z///M
AP/MM//MZv/Mmf/MzP/M////AP//M///Zv//mf//zP///wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAkMAB4ALAAAAAAbABsA
AAj/AD0IHEiw4MAvI748MsiwoYdHX75EiDDCoUUPrTK2khhh4UWDGhNpnFjx48CQI1olSjnR40eU
rQCMEMmxpEWNMmPO1Cjxy8VrQAFklCkyYyKSFoFeA3BNZ1GVIyIk8tlQKdOlO41GhWizoNWgQEek
XCmVYiCGSr8qFbsVoRW0aQNdTRv16IizBpWyYhVoRVqgrKxwfZv3WqDDh/9e48tKLF6vSgPtDfSX
sZURhAuyiouYMmDJVlY8Jrh3M1DPaS2vyEx68V7FnwOLbvi6tGLVrAtK3lza9GIrskczBM67txXg
oXM33O06MPLZJj0Q//1c+UXmz4VHP859dfThfaFHAg8IADs=
"""
delete_3 = """
R0lGODlhGwAbAPcAAAAAAAAAMwAAZgAAmQAAzAAA/wAzAAAzMwAzZgAzmQAzzAAz/wBmAABmMwBm
ZgBmmQBmzABm/wCZAACZMwCZZgCZmQCZzACZ/wDMAADMMwDMZgDMmQDMzADM/wD/AAD/MwD/ZgD/
mQD/zAD//zMAADMAMzMAZjMAmTMAzDMA/zMzADMzMzMzZjMzmTMzzDMz/zNmADNmMzNmZjNmmTNm
zDNm/zOZADOZMzOZZjOZmTOZzDOZ/zPMADPMMzPMZjPMmTPMzDPM/zP/ADP/MzP/ZjP/mTP/zDP/
/2YAAGYAM2YAZmYAmWYAzGYA/2YzAGYzM2YzZmYzmWYzzGYz/2ZmAGZmM2ZmZmZmmWZmzGZm/2aZ
AGaZM2aZZmaZmWaZzGaZ/2bMAGbMM2bMZmbMmWbMzGbM/2b/AGb/M2b/Zmb/mWb/zGb//5kAAJkA
M5kAZpkAmZkAzJkA/5kzAJkzM5kzZpkzmZkzzJkz/5lmAJlmM5lmZplmmZlmzJlm/5mZAJmZM5mZ
ZpmZmZmZzJmZ/5nMAJnMM5nMZpnMmZnMzJnM/5n/AJn/M5n/Zpn/mZn/zJn//8wAAMwAM8wAZswA
mcwAzMwA/8wzAMwzM8wzZswzmcwzzMwz/8xmAMxmM8xmZsxmmcxmzMxm/8yZAMyZM8yZZsyZmcyZ
zMyZ/8zMAMzMM8zMZszMmczMzMzM/8z/AMz/M8z/Zsz/mcz/zMz///8AAP8AM/8AZv8Amf8AzP8A
//8zAP8zM/8zZv8zmf8zzP8z//9mAP9mM/9mZv9mmf9mzP9m//+ZAP+ZM/+ZZv+Zmf+ZzP+Z///M
AP/MM//MZv/Mmf/MzP/M////AP//M///Zv//mf//zP///wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAkMAB4ALAAAAAAbABsA
AAjnAD0IHEiw4MABCAcYXMjQQ8KEDSN6SEQx0UOJCytWvIhxoEaNHDF+TATgC8WQDSuWJGlyI0KJ
12KuXAnyZcOY1wBcY/kRYQSFDHHqzNnS5U+gBoXKjFnzaKCFOJXiPDngqBWoUb8MjeoT4dOkMVu1
0rUiakxWVqxi1cWWrdlrrAKx8oo1pi6xuszGRTvgqkFWUdu6PSvXyoqvBVkpxplXb+EVfhPDXfwW
7mPEiQErBuwYLeSGcjVvxrnXMOaFVhZvVmwlteHIEUNPRuv6cEeBqTW39gy7o+zap2+3Hv75tkEr
gVbYNh4QADs=
"""
dkblnode = """
R0lGODdhGgAYALMAAAAAAIAAAACAAICAAAAAgIAAgACAgICAgMDAwP8AAAD/AP//AAAA//8A/wD/
/////ywAAAAAGgAYAAMEefDJSau9eILMJfgg2FGhADCfuY0h4zouKmYgA8N27nzcl7+nl4t3qeVA
NyCx8kkeY7ol6WT47XQxqeaEwwGcMO3D133GbuJxEGfj/mRFd3ItTJOh1/JuxfRh/zJ8fW9sUYJ9
ckJehxYtP1UhLCGTaTSUlSOZmpucGBEAOw==
"""
dkgnnode = """
R0lGODdhGgAYALMAAAAAAIAAAACAAICAAAAAgIAAgACAgICAgMDAwP8AAAD/AP//AAAA//8A/wD/
/////ywAAAAAGgAYAAMEhvDJSau9eILMJfgg2FHhZ5TjEyoHa7AhB7rG8BrtuWXf8eYAnEL4wYB+
OkAOWLR8hksFAEptVkwtH0zItZKUQ9Yw6HJ5PeQaDjfVas9orliJhO2u4PW4/L5/62w3S3AqPXVR
PiIXJlQ+XC+EaEEDfGIHkRpHSAJJfkYloCmZoJ6ipqeoqQ8RADs=
"""
edge = """
R0lGODdhIAAgAPcPAAAAAIAAAACAAICAAAAAgIAAgACAgMDAwICAgP8AAAD/AP//AAAA//8A/wD/
/////wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAEAAAEALAAAAAAgACAA
Bwh0AAMIHEiwoMGDCBMqXMiwocOHDAFIBABxIYAEGBNQrHjwYkaMGzkS9PgxpEiBJDOaPJkS5MmC
KSW+hDlx4kyENm8azKlzpMyePlf25Ak0AFGgR4f+LGp0aVGbNZXGHPrR5c2WGnViFcpxK1WVT5My
HUs2YUAAOw==
"""
edge_1 = """
R0lGODlhHgAeAPcAAAAAAAAAMwAAZgAAmQAAzAAA/wAzAAAzMwAzZgAzmQAzzAAz/wBmAABmMwBm
ZgBmmQBmzABm/wCZAACZMwCZZgCZmQCZzACZ/wDMAADMMwDMZgDMmQDMzADM/wD/AAD/MwD/ZgD/
mQD/zAD//zMAADMAMzMAZjMAmTMAzDMA/zMzADMzMzMzZjMzmTMzzDMz/zNmADNmMzNmZjNmmTNm
zDNm/zOZADOZMzOZZjOZmTOZzDOZ/zPMADPMMzPMZjPMmTPMzDPM/zP/ADP/MzP/ZjP/mTP/zDP/
/2YAAGYAM2YAZmYAmWYAzGYA/2YzAGYzM2YzZmYzmWYzzGYz/2ZmAGZmM2ZmZmZmmWZmzGZm/2aZ
AGaZM2aZZmaZmWaZzGaZ/2bMAGbMM2bMZmbMmWbMzGbM/2b/AGb/M2b/Zmb/mWb/zGb//5kAAJkA
M5kAZpkAmZkAzJkA/5kzAJkzM5kzZpkzmZkzzJkz/5lmAJlmM5lmZplmmZlmzJlm/5mZAJmZM5mZ
ZpmZmZmZzJmZ/5nMAJnMM5nMZpnMmZnMzJnM/5n/AJn/M5n/Zpn/mZn/zJn//8wAAMwAM8wAZswA
mcwAzMwA/8wzAMwzM8wzZswzmcwzzMwz/8xmAMxmM8xmZsxmmcxmzMxm/8yZAMyZM8yZZsyZmcyZ
zMyZ/8zMAMzMM8zMZszMmczMzMzM/8z/AMz/M8z/Zsz/mcz/zMz///8AAP8AM/8AZv8Amf8AzP8A
//8zAP8zM/8zZv8zmf8zzP8z//9mAP9mM/9mZv9mmf9mzP9m//+ZAP+ZM/+ZZv+Zmf+ZzP+Z///M
AP/MM//MZv/Mmf/MzP/M////AP//M///Zv//mf//zP///wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAkMAB4ALAAAAAAeAB4A
AAi3AD0IHEiwoEGBVqyssBLooMOHClldY7Wi4cOLAgMtDMQqkEKLGB1uXLiCVcKQED12XLkCpUgr
rCTG1OjyoEaJ1yYurGnwGoCSOTfyJOgTAACFH4cO9FmRIUOQQ5lCVbr0pxWqPX9Oxcr0KlaiWr+C
3SlWoNSyZq2i9XAWbVeqC8O2HWoFgMyfapUGstpxhVEAWBciJdkSKwCVM+t+PRpzYt+vdXFOPPp1
L9C5cPH63aq0YkXOPAMCADs=
"""
edge_2 = """
R0lGODlhHgAeAPcAAAAAAAAAMwAAZgAAmQAAzAAA/wAzAAAzMwAzZgAzmQAzzAAz/wBmAABmMwBm
ZgBmmQBmzABm/wCZAACZMwCZZgCZmQCZzACZ/wDMAADMMwDMZgDMmQDMzADM/wD/AAD/MwD/ZgD/
mQD/zAD//zMAADMAMzMAZjMAmTMAzDMA/zMzADMzMzMzZjMzmTMzzDMz/zNmADNmMzNmZjNmmTNm
zDNm/zOZADOZMzOZZjOZmTOZzDOZ/zPMADPMMzPMZjPMmTPMzDPM/zP/ADP/MzP/ZjP/mTP/zDP/
/2YAAGYAM2YAZmYAmWYAzGYA/2YzAGYzM2YzZmYzmWYzzGYz/2ZmAGZmM2ZmZmZmmWZmzGZm/2aZ
AGaZM2aZZmaZmWaZzGaZ/2bMAGbMM2bMZmbMmWbMzGbM/2b/AGb/M2b/Zmb/mWb/zGb//5kAAJkA
M5kAZpkAmZkAzJkA/5kzAJkzM5kzZpkzmZkzzJkz/5lmAJlmM5lmZplmmZlmzJlm/5mZAJmZM5mZ
ZpmZmZmZzJmZ/5nMAJnMM5nMZpnMmZnMzJnM/5n/AJn/M5n/Zpn/mZn/zJn//8wAAMwAM8wAZswA
mcwAzMwA/8wzAMwzM8wzZswzmcwzzMwz/8xmAMxmM8xmZsxmmcxmzMxm/8yZAMyZM8yZZsyZmcyZ
zMyZ/8zMAMzMM8zMZszMmczMzMzM/8z/AMz/M8z/Zsz/mcz/zMz///8AAP8AM/8AZv8Amf8AzP8A
//8zAP8zM/8zZv8zmf8zzP8z//9mAP9mM/9mZv9mmf9mzP9m//+ZAP+ZM/+ZZv+Zmf+ZzP+Z///M
AP/MM//MZv/Mmf/MzP/M////AP//M///Zv//mf//zP///wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAkMAB4ALAAAAAAeAB4A
AAjEAD0IHEiwoEGBVqyssBLooMOHClldY7Wi4cOLAgMtDMQqkEKLGB1uXLiCVcKQED12XLkCpUgr
rCTG1OjyoEaJ1yYurGlwAICSOTfyJPgoghUACj8OHXgtwpcBDBmCHFp0hJWlB5smmopVYNWrXQk2
/cK169ewYiNsRTvwLFsPY8tidctWq9yQC382pMvzqMyfTu9iDPQTpkYAULsuTEqyZVcAKmceDYs0
5sSOjrH6zTkRaVjCQK/pRbvip2nBNStWRI0yIAA7
"""
edge_3 = """
R0lGODlhHgAeAPcAAAAAAAAAMwAAZgAAmQAAzAAA/wAzAAAzMwAzZgAzmQAzzAAz/wBmAABmMwBm
ZgBmmQBmzABm/wCZAACZMwCZZgCZmQCZzACZ/wDMAADMMwDMZgDMmQDMzADM/wD/AAD/MwD/ZgD/
mQD/zAD//zMAADMAMzMAZjMAmTMAzDMA/zMzADMzMzMzZjMzmTMzzDMz/zNmADNmMzNmZjNmmTNm
zDNm/zOZADOZMzOZZjOZmTOZzDOZ/zPMADPMMzPMZjPMmTPMzDPM/zP/ADP/MzP/ZjP/mTP/zDP/
/2YAAGYAM2YAZmYAmWYAzGYA/2YzAGYzM2YzZmYzmWYzzGYz/2ZmAGZmM2ZmZmZmmWZmzGZm/2aZ
AGaZM2aZZmaZmWaZzGaZ/2bMAGbMM2bMZmbMmWbMzGbM/2b/AGb/M2b/Zmb/mWb/zGb//5kAAJkA
M5kAZpkAmZkAzJkA/5kzAJkzM5kzZpkzmZkzzJkz/5lmAJlmM5lmZplmmZlmzJlm/5mZAJmZM5mZ
ZpmZmZmZzJmZ/5nMAJnMM5nMZpnMmZnMzJnM/5n/AJn/M5n/Zpn/mZn/zJn//8wAAMwAM8wAZswA
mcwAzMwA/8wzAMwzM8wzZswzmcwzzMwz/8xmAMxmM8xmZsxmmcxmzMxm/8yZAMyZM8yZZsyZmcyZ
zMyZ/8zMAMzMM8zMZszMmczMzMzM/8z/AMz/M8z/Zsz/mcz/zMz///8AAP8AM/8AZv8Amf8AzP8A
//8zAP8zM/8zZv8zmf8zzP8z//9mAP9mM/9mZv9mmf9mzP9m//+ZAP+ZM/+ZZv+Zmf+ZzP+Z///M
AP/MM//MZv/Mmf/MzP/M////AP//M///Zv//mf//zP///wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAkMAB4ALAAAAAAeAB4A
AAjCAD0IHEiwoEGBVqyssBLooMOHClldY7Wi4cOLAgMtDMQqkEKLGB1uXLiCVcKQED12XLkCpUgr
rCTG1OjyoEaJ1yYurGlwAICSOTfyJPhlgAAACj8OHdhqgFGGDEEOLTpA6lKmTq1cNUjV6tamA7Ru
JerU61WwYscK7KoWa9i2a8vC9YD26sKfDdkutQJA5s+sVwP9hKkRQNWtC5OSbLkVgMqZfMcijTmx
I+OrfHFORDpWMNBreNWu+Ena7NCKFU3XDAgAOw==
"""
edit = """
R0lGODdhIAAgAPcPAAAAAIAAAACAAICAAAAAgIAAgACAgMDAwICAgP8AAAD/AP//AAAA//8A/wD/
/////wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAEAAAAALAAAAAAgACAA
Bwh0AAEIHEiwoMGDCBMqXMgQgUMEDCMCeEgRosSEDglmvHhwo0aLHD8a9BhSIEmTIEuiFKnyY8WU
LVGevPiyZs2GNnPOHKkzJ86eNxcCtflzKMWiRncWTFoRaVKnRqEOlQqUak+rOrH6FMr0oVaiMcOK
HUu2ZEAAOw==
"""
edit_1 = """
R0lGODlhIAAgAPcAAAAAAAAAMwAAZgAAmQAAzAAA/wAzAAAzMwAzZgAzmQAzzAAz/wBmAABmMwBm
ZgBmmQBmzABm/wCZAACZMwCZZgCZmQCZzACZ/wDMAADMMwDMZgDMmQDMzADM/wD/AAD/MwD/ZgD/
mQD/zAD//zMAADMAMzMAZjMAmTMAzDMA/zMzADMzMzMzZjMzmTMzzDMz/zNmADNmMzNmZjNmmTNm
zDNm/zOZADOZMzOZZjOZmTOZzDOZ/zPMADPMMzPMZjPMmTPMzDPM/zP/ADP/MzP/ZjP/mTP/zDP/
/2YAAGYAM2YAZmYAmWYAzGYA/2YzAGYzM2YzZmYzmWYzzGYz/2ZmAGZmM2ZmZmZmmWZmzGZm/2aZ
AGaZM2aZZmaZmWaZzGaZ/2bMAGbMM2bMZmbMmWbMzGbM/2b/AGb/M2b/Zmb/mWb/zGb//5kAAJkA
M5kAZpkAmZkAzJkA/5kzAJkzM5kzZpkzmZkzzJkz/5lmAJlmM5lmZplmmZlmzJlm/5mZAJmZM5mZ
ZpmZmZmZzJmZ/5nMAJnMM5nMZpnMmZnMzJnM/5n/AJn/M5n/Zpn/mZn/zJn//8wAAMwAM8wAZswA
mcwAzMwA/8wzAMwzM8wzZswzmcwzzMwz/8xmAMxmM8xmZsxmmcxmzMxm/8yZAMyZM8yZZsyZmcyZ
zMyZ/8zMAMzMM8zMZszMmczMzMzM/8z/AMz/M8z/Zsz/mcz/zMz///8AAP8AM/8AZv8Amf8AzP8A
//8zAP8zM/8zZv8zmf8zzP8z//9mAP9mM/9mZv9mmf9mzP9m//+ZAP+ZM/+ZZv+Zmf+ZzP+Z///M
AP/MM//MZv/Mmf/MzP/M////AP//M///Zv//mf//zP///wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAkMAB4ALAAAAAAgACAA
AAj/AD0IHEiwoC5dBRMqXEjwIMOHEB1CnOghkBUrAKwczAigI8WCFq2wYhWI1cGSJTEC+HiR1bWX
MA/ChGlxxcOWM2fKzPky0AqbCUXy1KlrKE2gA4XOBJBzJ0ymM30mdZkTasyiS3lKLWkUqpVrB7++
tJozEACqQ72C1SX2GtmyQ9sy/Rp2LMy2RtuK9VrX7Uu9Rq8B9quRrV/BdwP/XezWoswVahFLNssY
caCUGcMCWGGRsVizHiJbsUh6ReGfVkybZsxqpUC1FhNKJAhZbOuCXlm97thxI2/XGFfcTgig5GuD
CAkC97jQteuGyZUff8gcucLqH6Fnzz57e8To3h92Ew+/cDx52eDPo1fP0Dx7ge4XBgQAOw==
"""
edit_2 = """
R0lGODlhIAAgAPcAAAAAAAAAMwAAZgAAmQAAzAAA/wAzAAAzMwAzZgAzmQAzzAAz/wBmAABmMwBm
ZgBmmQBmzABm/wCZAACZMwCZZgCZmQCZzACZ/wDMAADMMwDMZgDMmQDMzADM/wD/AAD/MwD/ZgD/
mQD/zAD//zMAADMAMzMAZjMAmTMAzDMA/zMzADMzMzMzZjMzmTMzzDMz/zNmADNmMzNmZjNmmTNm
zDNm/zOZADOZMzOZZjOZmTOZzDOZ/zPMADPMMzPMZjPMmTPMzDPM/zP/ADP/MzP/ZjP/mTP/zDP/
/2YAAGYAM2YAZmYAmWYAzGYA/2YzAGYzM2YzZmYzmWYzzGYz/2ZmAGZmM2ZmZmZmmWZmzGZm/2aZ
AGaZM2aZZmaZmWaZzGaZ/2bMAGbMM2bMZmbMmWbMzGbM/2b/AGb/M2b/Zmb/mWb/zGb//5kAAJkA
M5kAZpkAmZkAzJkA/5kzAJkzM5kzZpkzmZkzzJkz/5lmAJlmM5lmZplmmZlmzJlm/5mZAJmZM5mZ
ZpmZmZmZzJmZ/5nMAJnMM5nMZpnMmZnMzJnM/5n/AJn/M5n/Zpn/mZn/zJn//8wAAMwAM8wAZswA
mcwAzMwA/8wzAMwzM8wzZswzmcwzzMwz/8xmAMxmM8xmZsxmmcxmzMxm/8yZAMyZM8yZZsyZmcyZ
zMyZ/8zMAMzMM8zMZszMmczMzMzM/8z/AMz/M8z/Zsz/mcz/zMz///8AAP8AM/8AZv8Amf8AzP8A
//8zAP8zM/8zZv8zmf8zzP8z//9mAP9mM/9mZv9mmf9mzP9m//+ZAP+ZM/+ZZv+Zmf+ZzP+Z///M
AP/MM//MZv/Mmf/MzP/M////AP//M///Zv//mf//zP///wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAkMAB4ALAAAAAAgACAA
AAj/AHV5GEiwoMGDCAkKTMiwocKFDiMa1AXRYCArVgBk3Aigo0OKFjGyYhWIpMmSGQEwBDlQ5LWX
MGPGvLgiIUWXMnPmDLSi5kRdrHQK1cnToBWKMQEMhal0pk8PVlghZbr0ZVOnHkq+nGr1pRWYX7sS
BRB0q66kXsFSFRpI5k2qYa+FvRpX6NGzc9PKFRu3rsy7e68pDfu1ad+qSOde3LvCsN6wgRybDYRS
I8aMKxYHhqxS8s2LPK307Cm6tF5WKgc6DsTSYeOwqA0alqqro+3btlsCWBH7IICSA1snTJ2SYWoA
woerdujRQ3KEzSU6ryhd4vPqK6ljb3h9+8Hu3n+GC/+ofbx48zbLMwwIADs=
"""
edit_3 = """
R0lGODlhIAAgAPcAAAAAAAAAMwAAZgAAmQAAzAAA/wAzAAAzMwAzZgAzmQAzzAAz/wBmAABmMwBm
ZgBmmQBmzABm/wCZAACZMwCZZgCZmQCZzACZ/wDMAADMMwDMZgDMmQDMzADM/wD/AAD/MwD/ZgD/
mQD/zAD//zMAADMAMzMAZjMAmTMAzDMA/zMzADMzMzMzZjMzmTMzzDMz/zNmADNmMzNmZjNmmTNm
zDNm/zOZADOZMzOZZjOZmTOZzDOZ/zPMADPMMzPMZjPMmTPMzDPM/zP/ADP/MzP/ZjP/mTP/zDP/
/2YAAGYAM2YAZmYAmWYAzGYA/2YzAGYzM2YzZmYzmWYzzGYz/2ZmAGZmM2ZmZmZmmWZmzGZm/2aZ
AGaZM2aZZmaZmWaZzGaZ/2bMAGbMM2bMZmbMmWbMzGbM/2b/AGb/M2b/Zmb/mWb/zGb//5kAAJkA
M5kAZpkAmZkAzJkA/5kzAJkzM5kzZpkzmZkzzJkz/5lmAJlmM5lmZplmmZlmzJlm/5mZAJmZM5mZ
ZpmZmZmZzJmZ/5nMAJnMM5nMZpnMmZnMzJnM/5n/AJn/M5n/Zpn/mZn/zJn//8wAAMwAM8wAZswA
mcwAzMwA/8wzAMwzM8wzZswzmcwzzMwz/8xmAMxmM8xmZsxmmcxmzMxm/8yZAMyZM8yZZsyZmcyZ
zMyZ/8zMAMzMM8zMZszMmczMzMzM/8z/AMz/M8z/Zsz/mcz/zMz///8AAP8AM/8AZv8Amf8AzP8A
//8zAP8zM/8zZv8zmf8zzP8z//9mAP9mM/9mZv9mmf9mzP9m//+ZAP+ZM/+ZZv+Zmf+ZzP+Z///M
AP/MM//MZv/Mmf/MzP/M////AP//M///Zv//mf//zP///wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAkMAB4ALAAAAAAgACAA
AAj/AD0IHEiwoMGDAnXpQsiw4UCFDiMahNgwkBUrAKyswIgRAICDFA9atMKKVaCSJlN2JBiy4EVW
12LKnDnT4oqEC12SpMmzZ6AVLQXu7EmUZ9ChMgEUTSpT4c+BSJkuvaY0psJrT08SrWpFZteYVa9d
jRkIAMytMb9eUxt2LNmebNd6BdtU11K1X7mmpbv2qlqeeOl+7crV79TBYC3KXVFYF2KsevcGOslx
JIAVFhU+LushshWLk39eXEFao5WxX1l9FKjXYkSFjFOvHsiVVWePuHN7hIhxheqDAE6ybqhw9UqE
q2cjhJjcoUeHIZ9LhJ5zusSg1olXz66d+/Xt3kGCDA8/cTz5h+bP47QeEAA7
"""
gato = """
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
4hWDQPiGcBiHcjiHdFiHdniHeHiHgQAAOw==
"""
getgray = """
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
Ow==
"""
gnedge = """
R0lGODdhHgAeAPcAAH7+qX3+qX39qXz9qXv8qnv7qnz8qXv8qXv7qXr7qnr6qnn6qnj5q3f5q3b3
rG7xrgAAAAAAMwAAZgAAmQAAzAAA/wAzAAAzMwAzZgAzmQAzzAAz/wBmAABmMwBmZgBmmQBmzABm
/wCZAACZMwCZZgCZmQCZzACZ/wDMAADMMwDMZgDMmQDMzADM/wD/AAD/MwD/ZgD/mQD/zAD//zMA
ADMAMzMAZjMAmTMAzDMA/zMzADMzMzMzZjMzmTMzzDMz/zNmADNmMzNmZjNmmTNmzDNm/zOZADOZ
MzOZZjOZmTOZzDOZ/zPMADPMMzPMZjPMmTPMzDPM/zP/ADP/MzP/ZjP/mTP/zDP//2YAAGYAM2YA
ZmYAmWYAzGYA/2YzAGYzM2YzZmYzmWYzzGYz/2ZmAGZmM2ZmZmZmmWZmzGZm/2aZAGaZM2aZZmaZ
mWaZzGaZ/2bMAGbMM2bMZmbMmWbMzGbM/2b/AGb/M2b/Zmb/mWb/zGb//5kAAJkAM5kAZpkAmZkA
zJkA/5kzAJkzM5kzZpkzmZkzzJkz/5lmAJlmM5lmZplmmZlmzJlm/5mZAJmZM5mZZpmZmZmZzJmZ
/5nMAJnMM5nMZpnMmZnMzJnM/5n/AJn/M5n/Zpn/mZn/zJn//8wAAMwAM8wAZswAmcwAzMwA/8wz
AMwzM8wzZswzmcwzzMwz/8xmAMxmM8xmZsxmmcxmzMxm/8yZAMyZM8yZZsyZmcyZzMyZ/8zMAMzM
M8zMZszMmczMzMzM/8z/AMz/M8z/Zsz/mcz/zMz///8AAP8AM/8AZv8Amf8AzP8A//8zAP8zM/8z
Zv8zmf8zzP8z//9mAP9mM/9mZv9mmf9mzP9m//+ZAP+ZM/+ZZv+Zmf+ZzP+Z///MAP/MM//MZv/M
mf/MzP/M////AP//M///Zv//mf//zP///wAAAA0NDRoaGigoKDU1NUNDQ1BQUF1dXWtra3h4eIaG
hpOTk6Ghoa6urru7u8nJydbW1uTk5PHx8f///wAAAAEBAQICAgMDAywAAAAAHgAeAAcIrQAfOHCg
wIACBQIMGBDAEICAAAwjMoTY0OHAggolVgTAEUAAjh85CugYIOPIjihTqlwZcaXLlx0fwpxJs6bN
mzhz6tzJs6fPn0BVhgxKtKjRozs3BtW4VONQngkWajy5swGDAgwQDGCq04BWAgcWTG258qlIqQcO
CDiwdaxQkB8pMhzQduzYAHg9lnyIl2LdiFJbbhw5USTFwIHdniQ8eKLdxxKl9u0L+bFUAwF1ADs=
"""
gred = """
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
"""
greennde = """
R0lGODdhGgAYALMAAAAAAIAAAACAAICAAAAAgIAAgACAgICAgMDAwP8AAAD/AP//AAAA//8A/wD/
/////ywAAAAAGgAYAAMEWPDJSau9eILMJfgg2FHhp5TjEypse4oZ6M7sx5kzQNuX3PouWAWXo716
up2RN0wWd5um8bSMSpVQ5LKqxeasJOK0xryOyTEx9wYMCtMlVMoTL8/v+LxeEiEAOw==
"""
grey = """
R0lGODdhHgAeAPcAAL6+vgAAAAAAMwAAZgAAmQAAzAAA/wAzAAAzMwAzZgAzmQAzzAAz/wBmAABm
MwBmZgBmmQBmzABm/wCZAACZMwCZZgCZmQCZzACZ/wDMAADMMwDMZgDMmQDMzADM/wD/AAD/MwD/
ZgD/mQD/zAD//zMAADMAMzMAZjMAmTMAzDMA/zMzADMzMzMzZjMzmTMzzDMz/zNmADNmMzNmZjNm
mTNmzDNm/zOZADOZMzOZZjOZmTOZzDOZ/zPMADPMMzPMZjPMmTPMzDPM/zP/ADP/MzP/ZjP/mTP/
zDP//2YAAGYAM2YAZmYAmWYAzGYA/2YzAGYzM2YzZmYzmWYzzGYz/2ZmAGZmM2ZmZmZmmWZmzGZm
/2aZAGaZM2aZZmaZmWaZzGaZ/2bMAGbMM2bMZmbMmWbMzGbM/2b/AGb/M2b/Zmb/mWb/zGb//5kA
AJkAM5kAZpkAmZkAzJkA/5kzAJkzM5kzZpkzmZkzzJkz/5lmAJlmM5lmZplmmZlmzJlm/5mZAJmZ
M5mZZpmZmZmZzJmZ/5nMAJnMM5nMZpnMmZnMzJnM/5n/AJn/M5n/Zpn/mZn/zJn//8wAAMwAM8wA
ZswAmcwAzMwA/8wzAMwzM8wzZswzmcwzzMwz/8xmAMxmM8xmZsxmmcxmzMxm/8yZAMyZM8yZZsyZ
mcyZzMyZ/8zMAMzMM8zMZszMmczMzMzM/8z/AMz/M8z/Zsz/mcz/zMz///8AAP8AM/8AZv8Amf8A
zP8A//8zAP8zM/8zZv8zmf8zzP8z//9mAP9mM/9mZv9mmf9mzP9m//+ZAP+ZM/+ZZv+Zmf+ZzP+Z
///MAP/MM//MZv/Mmf/MzP/M////AP//M///Zv//mf//zP///wAAAA0NDRoaGigoKDU1NUNDQ1BQ
UF1dXWtra3h4eIaGhpOTk6Ghoa6urru7u8nJydbW1uTk5PHx8f///wAAAAEBAQICAgMDAwQEBAUF
BQYGBgcHBwgICAkJCQoKCgsLCwwMDA0NDQ4ODg8PDxAQEBERERISEiwAAAAAHgAeAAcIMgABCBxI
sKDBgwgTKlzIsKHDhxAjSpxIsaLFixgzatzIsaPHjyBDihxJsqTJkyhTngwoADs=
"""
greynode = """
R0lGODdhGgAYALMAAAAAAIAAAACAAICAAAAAgIAAgACAgICAgMDAwP8AAAD/AP//AAAA//8A/wD/
/////ywAAAAAGgAYAAMEXvDJSau9eILMJfgg2FHhh5TjEyJse4oZ6M7sx5kzQNuX3PouWAXnOuh2
GwsxeED2jjlW85ckQaNO5fVHez2721rVCgaPNcuu+IxW59jo8LuzQvJupXwqrt/7/4CBFREAOw==
"""
ltblnode = """
R0lGODdhGgAYALMAAAAAAIAAAACAAICAAAAAgIAAgACAgICAgMDAwP8AAAD/AP//AAAA//8A/wD/
/////ywAAAAAGgAYAAMEhfDJSau9eILMJfgg2FHhx5TjExoO67QhB7YOYxg2U38ykNMAF8vAu8yG
RxysWPncaqyTMupgkgA1Ggs7RG4sTpqOqNTZrJ4gdHjSutBp6lM9PX2b3KzOp10ag2NIUVRwKh8t
g1ldIn9yWmZVd2BhdYhEkn+UQ5GFeCUoKWmfmKGlpqeoDxEAOw==
"""
ltgnnode = """
R0lGODdhGgAYALMAAAAAAIAAAACAAICAAAAAgIAAgACAgICAgMDAwP8AAAD/AP//AAAA//8A/wD/
/////ywAAAAAGgAYAAMElvDJSau9eILMJfgg2FGgAAiOKY4l4iyP8yjqln2C+zrKHD8mzse1QBhz
u4Xjg8EheqlhD6q6fHa+pVH2QjAtQ6JMMXA8s0EwQDzzQpWzgI20fnNz8N23cs0q12ZTQHN0Z28o
WS57hWZYOXZehBpSD4YxSi9VVoBKLn5Gi3xDgW1nS5JqYWYukaGbIbCoQrGuI7a3uLkcEQA7
"""
pink = """
R0lGODdhHgAeAPcAAL8/vwAAAAAAMwAAZgAAmQAAzAAA/wAzAAAzMwAzZgAzmQAzzAAz/wBmAABm
MwBmZgBmmQBmzABm/wCZAACZMwCZZgCZmQCZzACZ/wDMAADMMwDMZgDMmQDMzADM/wD/AAD/MwD/
ZgD/mQD/zAD//zMAADMAMzMAZjMAmTMAzDMA/zMzADMzMzMzZjMzmTMzzDMz/zNmADNmMzNmZjNm
mTNmzDNm/zOZADOZMzOZZjOZmTOZzDOZ/zPMADPMMzPMZjPMmTPMzDPM/zP/ADP/MzP/ZjP/mTP/
zDP//2YAAGYAM2YAZmYAmWYAzGYA/2YzAGYzM2YzZmYzmWYzzGYz/2ZmAGZmM2ZmZmZmmWZmzGZm
/2aZAGaZM2aZZmaZmWaZzGaZ/2bMAGbMM2bMZmbMmWbMzGbM/2b/AGb/M2b/Zmb/mWb/zGb//5kA
AJkAM5kAZpkAmZkAzJkA/5kzAJkzM5kzZpkzmZkzzJkz/5lmAJlmM5lmZplmmZlmzJlm/5mZAJmZ
M5mZZpmZmZmZzJmZ/5nMAJnMM5nMZpnMmZnMzJnM/5n/AJn/M5n/Zpn/mZn/zJn//8wAAMwAM8wA
ZswAmcwAzMwA/8wzAMwzM8wzZswzmcwzzMwz/8xmAMxmM8xmZsxmmcxmzMxm/8yZAMyZM8yZZsyZ
mcyZzMyZ/8zMAMzMM8zMZszMmczMzMzM/8z/AMz/M8z/Zsz/mcz/zMz///8AAP8AM/8AZv8Amf8A
zP8A//8zAP8zM/8zZv8zmf8zzP8z//9mAP9mM/9mZv9mmf9mzP9m//+ZAP+ZM/+ZZv+Zmf+ZzP+Z
///MAP/MM//MZv/Mmf/MzP/M////AP//M///Zv//mf//zP///wAAAA0NDRoaGigoKDU1NUNDQ1BQ
UF1dXWtra3h4eIaGhpOTk6Ghoa6urru7u8nJydbW1uTk5PHx8f///wAAAAEBAQICAgMDAwQEBAUF
BQYGBgcHBwgICAkJCQoKCgsLCwwMDA0NDQ4ODg8PDxAQEBERERISEiwAAAAAHgAeAAcIMgABCBxI
sKDBgwgTKlzIsKHDhxAjSpxIsaLFixgzatzIsaPHjyBDihxJsqTJkyhTngwoADs=
"""
red = """
R0lGODdhHgAeAPcAAP8AAAAAAAAAMwAAZgAAmQAAzAAA/wAzAAAzMwAzZgAzmQAzzAAz/wBmAABm
MwBmZgBmmQBmzABm/wCZAACZMwCZZgCZmQCZzACZ/wDMAADMMwDMZgDMmQDMzADM/wD/AAD/MwD/
ZgD/mQD/zAD//zMAADMAMzMAZjMAmTMAzDMA/zMzADMzMzMzZjMzmTMzzDMz/zNmADNmMzNmZjNm
mTNmzDNm/zOZADOZMzOZZjOZmTOZzDOZ/zPMADPMMzPMZjPMmTPMzDPM/zP/ADP/MzP/ZjP/mTP/
zDP//2YAAGYAM2YAZmYAmWYAzGYA/2YzAGYzM2YzZmYzmWYzzGYz/2ZmAGZmM2ZmZmZmmWZmzGZm
/2aZAGaZM2aZZmaZmWaZzGaZ/2bMAGbMM2bMZmbMmWbMzGbM/2b/AGb/M2b/Zmb/mWb/zGb//5kA
AJkAM5kAZpkAmZkAzJkA/5kzAJkzM5kzZpkzmZkzzJkz/5lmAJlmM5lmZplmmZlmzJlm/5mZAJmZ
M5mZZpmZmZmZzJmZ/5nMAJnMM5nMZpnMmZnMzJnM/5n/AJn/M5n/Zpn/mZn/zJn//8wAAMwAM8wA
ZswAmcwAzMwA/8wzAMwzM8wzZswzmcwzzMwz/8xmAMxmM8xmZsxmmcxmzMxm/8yZAMyZM8yZZsyZ
mcyZzMyZ/8zMAMzMM8zMZszMmczMzMzM/8z/AMz/M8z/Zsz/mcz/zMz///8AAP8AM/8AZv8Amf8A
zP8A//8zAP8zM/8zZv8zmf8zzP8z//9mAP9mM/9mZv9mmf9mzP9m//+ZAP+ZM/+ZZv+Zmf+ZzP+Z
///MAP/MM//MZv/Mmf/MzP/M////AP//M///Zv//mf//zP///wAAAA0NDRoaGigoKDU1NUNDQ1BQ
UF1dXWtra3h4eIaGhpOTk6Ghoa6urru7u8nJydbW1uTk5PHx8f///wAAAAEBAQICAgMDAwQEBAUF
BQYGBgcHBwgICAkJCQoKCgsLCwwMDA0NDQ4ODg8PDxAQEBERERISEiwAAAAAHgAeAAcIMgABCBxI
sKDBgwgTKlzIsKHDhxAjSpxIsaLFixgzatzIsaPHjyBDihxJsqTJkyhTngwoADs=
"""
rednode = """
R0lGODdhGgAYAPEAAP///wAAAP8AAL8/vywAAAAAGgAYAAACU4SPqcsXDU2YlEZUp8gXVAGGm9VQ
4glOkHYGqLqYoSySCduiY+y+Onzr5V6PoG7zKxp9RN4v6WS2lBjcMQVcXrElK3RFq9m6GU5HUs6e
1+y221AAADs=
"""
swap = """
R0lGODdhIAAgAPcPAAAAAIAAAACAAICAAAAAgIAAgACAgMDAwICAgP8AAAD/AP//AAAA//8A/wD/
/////wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAEAAAEALAAAAAAgACAA
BwixAAMIHEiwoMGDCBMGSKCwYcIEDB1KHAgx4kSHFS1eDACgY8GMGicCqAiAIsiMIkkKPMkypMGR
EEuubAmyYUeZJivm1LlxJsSPKBHePMiTYM2DQ4m6XBi0YFKiSosSfHrx6FSqE60OvImzqlSBXHv6
dBlW6MOfL7Fu7WoUbVqPb6MmLHsVbk+uXfE6tSsUr1+7MBOwffu3bODBSAsDVnmXLli+Fx2LjTt5
LuLKWzFrHhgQADs=
"""
swap_1 = """
R0lGODlhHgAeAPcAAAAAAAAAMwAAZgAAmQAAzAAA/wAzAAAzMwAzZgAzmQAzzAAz/wBmAABmMwBm
ZgBmmQBmzABm/wCZAACZMwCZZgCZmQCZzACZ/wDMAADMMwDMZgDMmQDMzADM/wD/AAD/MwD/ZgD/
mQD/zAD//zMAADMAMzMAZjMAmTMAzDMA/zMzADMzMzMzZjMzmTMzzDMz/zNmADNmMzNmZjNmmTNm
zDNm/zOZADOZMzOZZjOZmTOZzDOZ/zPMADPMMzPMZjPMmTPMzDPM/zP/ADP/MzP/ZjP/mTP/zDP/
/2YAAGYAM2YAZmYAmWYAzGYA/2YzAGYzM2YzZmYzmWYzzGYz/2ZmAGZmM2ZmZmZmmWZmzGZm/2aZ
AGaZM2aZZmaZmWaZzGaZ/2bMAGbMM2bMZmbMmWbMzGbM/2b/AGb/M2b/Zmb/mWb/zGb//5kAAJkA
M5kAZpkAmZkAzJkA/5kzAJkzM5kzZpkzmZkzzJkz/5lmAJlmM5lmZplmmZlmzJlm/5mZAJmZM5mZ
ZpmZmZmZzJmZ/5nMAJnMM5nMZpnMmZnMzJnM/5n/AJn/M5n/Zpn/mZn/zJn//8wAAMwAM8wAZswA
mcwAzMwA/8wzAMwzM8wzZswzmcwzzMwz/8xmAMxmM8xmZsxmmcxmzMxm/8yZAMyZM8yZZsyZmcyZ
zMyZ/8zMAMzMM8zMZszMmczMzMzM/8z/AMz/M8z/Zsz/mcz/zMz///8AAP8AM/8AZv8Amf8AzP8A
//8zAP8zM/8zZv8zmf8zzP8z//9mAP9mM/9mZv9mmf9mzP9m//+ZAP+ZM/+ZZv+Zmf+ZzP+Z///M
AP/MM//MZv/Mmf/MzP/M////AP//M///Zv//mf//zP///wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAkMAB4ALAAAAAAeAB4A
AAjEAD0IHEiwoEGBVqyssBLooMOHClldY7Wi4cOLAgMtDMQqkEKLGB1uXLiCVcKQED12XLkCpUgr
rCTG1OjyoEaJ1yYurGkwEICSOTfWBEC0aFGFH3l68OiR6EaGIJVeO6r04NSFAKJW9XC1IQArWwde
Besha1iuP0F+DTu2oFaXXc8SbCtXYNy6aHdWxVox71uUVgDI/PmTrFKfCzuuKBp2IVKSLcNmhTkz
8NmvMScqPhsY58S1YRHjTCt3sWmzdStW/MszIAA7
"""
swap_2 = """
R0lGODlhHgAeAPcAAAAAAAAAMwAAZgAAmQAAzAAA/wAzAAAzMwAzZgAzmQAzzAAz/wBmAABmMwBm
ZgBmmQBmzABm/wCZAACZMwCZZgCZmQCZzACZ/wDMAADMMwDMZgDMmQDMzADM/wD/AAD/MwD/ZgD/
mQD/zAD//zMAADMAMzMAZjMAmTMAzDMA/zMzADMzMzMzZjMzmTMzzDMz/zNmADNmMzNmZjNmmTNm
zDNm/zOZADOZMzOZZjOZmTOZzDOZ/zPMADPMMzPMZjPMmTPMzDPM/zP/ADP/MzP/ZjP/mTP/zDP/
/2YAAGYAM2YAZmYAmWYAzGYA/2YzAGYzM2YzZmYzmWYzzGYz/2ZmAGZmM2ZmZmZmmWZmzGZm/2aZ
AGaZM2aZZmaZmWaZzGaZ/2bMAGbMM2bMZmbMmWbMzGbM/2b/AGb/M2b/Zmb/mWb/zGb//5kAAJkA
M5kAZpkAmZkAzJkA/5kzAJkzM5kzZpkzmZkzzJkz/5lmAJlmM5lmZplmmZlmzJlm/5mZAJmZM5mZ
ZpmZmZmZzJmZ/5nMAJnMM5nMZpnMmZnMzJnM/5n/AJn/M5n/Zpn/mZn/zJn//8wAAMwAM8wAZswA
mcwAzMwA/8wzAMwzM8wzZswzmcwzzMwz/8xmAMxmM8xmZsxmmcxmzMxm/8yZAMyZM8yZZsyZmcyZ
zMyZ/8zMAMzMM8zMZszMmczMzMzM/8z/AMz/M8z/Zsz/mcz/zMz///8AAP8AM/8AZv8Amf8AzP8A
//8zAP8zM/8zZv8zmf8zzP8z//9mAP9mM/9mZv9mmf9mzP9m//+ZAP+ZM/+ZZv+Zmf+ZzP+Z///M
AP/MM//MZv/Mmf/MzP/M////AP//M///Zv//mf//zP///wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAkMAB4ALAAAAAAeAB4A
AAjVAD0IHEiwoEGBVqyssBLooMOHClldY7Wi4cOLAgMtDMQqkEKLGB1uXLiCVcKQED12XLkCpUgr
rCTG1OjyoEaJ1yYurGlwAICSOTfyJPgoghUACj8OHXgtwpcBDBmCHFp0xNSlTCMksoLVYNWrXZt+
4dqVaASrXUcQbLq1bASLX8t6GMFVLFm3geLKnftF612eC382jOAULMqjMn+OOMtYrctAP2FqBCDA
o2WGLhcmJdmyKwCVM4+WRRpzYsfOWBHnnIi0LGSg1wTLXfGztuGhFSverhkQADs=
"""
swap_3 = """
R0lGODlhHgAeAPcAAAAAAAAAMwAAZgAAmQAAzAAA/wAzAAAzMwAzZgAzmQAzzAAz/wBmAABmMwBm
ZgBmmQBmzABm/wCZAACZMwCZZgCZmQCZzACZ/wDMAADMMwDMZgDMmQDMzADM/wD/AAD/MwD/ZgD/
mQD/zAD//zMAADMAMzMAZjMAmTMAzDMA/zMzADMzMzMzZjMzmTMzzDMz/zNmADNmMzNmZjNmmTNm
zDNm/zOZADOZMzOZZjOZmTOZzDOZ/zPMADPMMzPMZjPMmTPMzDPM/zP/ADP/MzP/ZjP/mTP/zDP/
/2YAAGYAM2YAZmYAmWYAzGYA/2YzAGYzM2YzZmYzmWYzzGYz/2ZmAGZmM2ZmZmZmmWZmzGZm/2aZ
AGaZM2aZZmaZmWaZzGaZ/2bMAGbMM2bMZmbMmWbMzGbM/2b/AGb/M2b/Zmb/mWb/zGb//5kAAJkA
M5kAZpkAmZkAzJkA/5kzAJkzM5kzZpkzmZkzzJkz/5lmAJlmM5lmZplmmZlmzJlm/5mZAJmZM5mZ
ZpmZmZmZzJmZ/5nMAJnMM5nMZpnMmZnMzJnM/5n/AJn/M5n/Zpn/mZn/zJn//8wAAMwAM8wAZswA
mcwAzMwA/8wzAMwzM8wzZswzmcwzzMwz/8xmAMxmM8xmZsxmmcxmzMxm/8yZAMyZM8yZZsyZmcyZ
zMyZ/8zMAMzMM8zMZszMmczMzMzM/8z/AMz/M8z/Zsz/mcz/zMz///8AAP8AM/8AZv8Amf8AzP8A
//8zAP8zM/8zZv8zmf8zzP8z//9mAP9mM/9mZv9mmf9mzP9m//+ZAP+ZM/+ZZv+Zmf+ZzP+Z///M
AP/MM//MZv/Mmf/MzP/M////AP//M///Zv//mf//zP///wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAkMAB4ALAAAAAAeAB4A
AAjUAD0IHEiwoEGBVqyssBLooMOHClldY7Wi4cOLAgMtDMQqkEKLGB1uXLiCVcKQED12XLkCpUgr
rCTG1OjyoEaJ1yYurGkwEICSOTfWHEC06AABABR+5OnBo0cBRxkyBMm0VVErTA9aHWBlANWsHrZi
5Qp24FaLXsuGJYpVIFmXAwieLfj1Ytq1b8uSnavWg1exfd12vct04c+GV8FaASDzp9GiNX0u7LgC
gACnUuu+VEqyZVkAKmcuVps05kTKahfjnJhUrWSch/tWng06sIeKFTXzDAgAOw==
"""
vertex = """
R0lGODdhIAAgAPcPAAAAAIAAAACAAICAAAAAgIAAgACAgMDAwICAgP8AAAD/AP//AAAA//8A/wD/
/////wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAEAAAEALAAAAAAgACAA
BwixAAMIHEiwoMGDCBMqXMiwocMACCJKRPAQYcSDFysKzJiQY0OKDkEyFEkQgMmTAAaS7FgQpUuT
EEeWPJmgps0EKFcaFEnzpk+cAHQS5Gnyp1GTQjcOLGr0aEqLApk2dQo1gNSpPmFitAoA61StBSle
9XoT7FCuZKliHJsWqEW2ac2G7dq27NO1de0qhItVrsGebf3+pRv37l6+WQUfBNz0ZEWUThUvfOlS
4+DKljNrzhwQADs=
"""
vertex_1 = """
R0lGODlhGAAZAPcAAAAAAAAAMwAAZgAAmQAAzAAA/wAzAAAzMwAzZgAzmQAzzAAz/wBmAABmMwBm
ZgBmmQBmzABm/wCZAACZMwCZZgCZmQCZzACZ/wDMAADMMwDMZgDMmQDMzADM/wD/AAD/MwD/ZgD/
mQD/zAD//zMAADMAMzMAZjMAmTMAzDMA/zMzADMzMzMzZjMzmTMzzDMz/zNmADNmMzNmZjNmmTNm
zDNm/zOZADOZMzOZZjOZmTOZzDOZ/zPMADPMMzPMZjPMmTPMzDPM/zP/ADP/MzP/ZjP/mTP/zDP/
/2YAAGYAM2YAZmYAmWYAzGYA/2YzAGYzM2YzZmYzmWYzzGYz/2ZmAGZmM2ZmZmZmmWZmzGZm/2aZ
AGaZM2aZZmaZmWaZzGaZ/2bMAGbMM2bMZmbMmWbMzGbM/2b/AGb/M2b/Zmb/mWb/zGb//5kAAJkA
M5kAZpkAmZkAzJkA/5kzAJkzM5kzZpkzmZkzzJkz/5lmAJlmM5lmZplmmZlmzJlm/5mZAJmZM5mZ
ZpmZmZmZzJmZ/5nMAJnMM5nMZpnMmZnMzJnM/5n/AJn/M5n/Zpn/mZn/zJn//8wAAMwAM8wAZswA
mcwAzMwA/8wzAMwzM8wzZswzmcwzzMwz/8xmAMxmM8xmZsxmmcxmzMxm/8yZAMyZM8yZZsyZmcyZ
zMyZ/8zMAMzMM8zMZszMmczMzMzM/8z/AMz/M8z/Zsz/mcz/zMz///8AAP8AM/8AZv8Amf8AzP8A
//8zAP8zM/8zZv8zmf8zzP8z//9mAP9mM/9mZv9mmf9mzP9m//+ZAP+ZM/+ZZv+Zmf+ZzP+Z///M
AP/MM//MZv/Mmf/MzP/M////AP//M///Zv//mf//zP///wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAkMAB4ALAAAAAAYABkA
AAj2AD0IHOghkBUrAFZYWWEwEMGHAw0mXJGw4gqFViAOXAhgoZVA11h9XKjQIcRACT+yYnWtZchA
BjE+PAgg0EqXOFnFLBmR4oqbK1fCBCn04kGBPht+HArzYEmSGT2kPEi1qlWoKzx4ZPi0qciYHhfG
TKnUYNCvI7ciVNjVytmVHg2KvZgSo0qbeOWSDKQQQEe7TAMrJcm2q1KdaOUqpRjX5tugganyVSj0
8VnHrC5K9Mgy582cVaWWtImzdEudJJGSJW369EqeBRE2/Fz6tdGoWinCPOvyLGOTERH+3f0SY0qN
TivOXdsR+EmaPi2O1Piw7EiDGgMCADs=
"""
vertex_2 = """
R0lGODlhGAAZAPcAAAAAAAAAMwAAZgAAmQAAzAAA/wAzAAAzMwAzZgAzmQAzzAAz/wBmAABmMwBm
ZgBmmQBmzABm/wCZAACZMwCZZgCZmQCZzACZ/wDMAADMMwDMZgDMmQDMzADM/wD/AAD/MwD/ZgD/
mQD/zAD//zMAADMAMzMAZjMAmTMAzDMA/zMzADMzMzMzZjMzmTMzzDMz/zNmADNmMzNmZjNmmTNm
zDNm/zOZADOZMzOZZjOZmTOZzDOZ/zPMADPMMzPMZjPMmTPMzDPM/zP/ADP/MzP/ZjP/mTP/zDP/
/2YAAGYAM2YAZmYAmWYAzGYA/2YzAGYzM2YzZmYzmWYzzGYz/2ZmAGZmM2ZmZmZmmWZmzGZm/2aZ
AGaZM2aZZmaZmWaZzGaZ/2bMAGbMM2bMZmbMmWbMzGbM/2b/AGb/M2b/Zmb/mWb/zGb//5kAAJkA
M5kAZpkAmZkAzJkA/5kzAJkzM5kzZpkzmZkzzJkz/5lmAJlmM5lmZplmmZlmzJlm/5mZAJmZM5mZ
ZpmZmZmZzJmZ/5nMAJnMM5nMZpnMmZnMzJnM/5n/AJn/M5n/Zpn/mZn/zJn//8wAAMwAM8wAZswA
mcwAzMwA/8wzAMwzM8wzZswzmcwzzMwz/8xmAMxmM8xmZsxmmcxmzMxm/8yZAMyZM8yZZsyZmcyZ
zMyZ/8zMAMzMM8zMZszMmczMzMzM/8z/AMz/M8z/Zsz/mcz/zMz///8AAP8AM/8AZv8Amf8AzP8A
//8zAP8zM/8zZv8zmf8zzP8z//9mAP9mM/9mZv9mmf9mzP9m//+ZAP+ZM/+ZZv+Zmf+ZzP+Z///M
AP/MM//MZv/Mmf/MzP/M////AP//M///Zv//mf//zP///wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAkMAB4ALAAAAAAYABkA
AAj/AD0IHOghUCsrA75Y+RLISiCCEAdaSZTwS8KLXxQ+jCgwkMVEXxK1uvZIJMiQVjgiDNlqxIhr
MK+NaNUKZKKNAwNRbPkyZsxHNTMmSilwYcIRj0i6RNoq6aOZQjda/FKzKtCriWyWFCowYdavYMNm
FfqlYEiqLBMBVcv27NiDU03WLIkUKduSNkN6DVmS7lKXWt9m3Pu1qWGgNceiRTjAZtaWkGdKNnlS
odCqVRNNzizXqFaXT0PXfdoS71ChS0X/He0yo8PGIXv+ROqTJN6HXuvW3v30pMC4S3fHXIqy4061
tHmPyBgSp1GaoGU/bd2YaE6KsGk6bRWSIkOVIC+CFeTuvTrHjhMxYhyK83xBh/AbNuQYEAA7
"""
vertex_3 = """
R0lGODlhGAAZAPcAAAAAAAAAMwAAZgAAmQAAzAAA/wAzAAAzMwAzZgAzmQAzzAAz/wBmAABmMwBm
ZgBmmQBmzABm/wCZAACZMwCZZgCZmQCZzACZ/wDMAADMMwDMZgDMmQDMzADM/wD/AAD/MwD/ZgD/
mQD/zAD//zMAADMAMzMAZjMAmTMAzDMA/zMzADMzMzMzZjMzmTMzzDMz/zNmADNmMzNmZjNmmTNm
zDNm/zOZADOZMzOZZjOZmTOZzDOZ/zPMADPMMzPMZjPMmTPMzDPM/zP/ADP/MzP/ZjP/mTP/zDP/
/2YAAGYAM2YAZmYAmWYAzGYA/2YzAGYzM2YzZmYzmWYzzGYz/2ZmAGZmM2ZmZmZmmWZmzGZm/2aZ
AGaZM2aZZmaZmWaZzGaZ/2bMAGbMM2bMZmbMmWbMzGbM/2b/AGb/M2b/Zmb/mWb/zGb//5kAAJkA
M5kAZpkAmZkAzJkA/5kzAJkzM5kzZpkzmZkzzJkz/5lmAJlmM5lmZplmmZlmzJlm/5mZAJmZM5mZ
ZpmZmZmZzJmZ/5nMAJnMM5nMZpnMmZnMzJnM/5n/AJn/M5n/Zpn/mZn/zJn//8wAAMwAM8wAZswA
mcwAzMwA/8wzAMwzM8wzZswzmcwzzMwz/8xmAMxmM8xmZsxmmcxmzMxm/8yZAMyZM8yZZsyZmcyZ
zMyZ/8zMAMzMM8zMZszMmczMzMzM/8z/AMz/M8z/Zsz/mcz/zMz///8AAP8AM/8AZv8Amf8AzP8A
//8zAP8zM/8zZv8zmf8zzP8z//9mAP9mM/9mZv9mmf9mzP9m//+ZAP+ZM/+ZZv+Zmf+ZzP+Z///M
AP/MM//MZv/Mmf/MzP/M////AP//M///Zv//mf//zP///wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAkMAB4ALAAAAAAYABkA
AAj/AD0IHOghkJUIAgZYGRAoQiCCEAc2TDggocUBAxxGHLiQYoRE11pF+BIBYwQrGwNRHNCq1bWX
Ib+QzPiQ4EGKiVzCDNlqZkaUAgNVxJizZUuZX4x+MVlTgMeMSJFGKDmyZEaBCaeanMq1q9aMAwoK
oEqyLMlEVcFq/SKUbFWSRkWW1UrVqVu4cUVCXauQatekgHuS/LrUit0Bg6Mqnmu1ot+Rc3vKrfr2
5FizeeMqpqq2bKucRT8nSvSlKEYrKp3C3Ql6Z0itD8dWXO0a5mefAh1P/azTtVGMAoDe3NtyJ+9E
GGlKHFoyNE/RjoFyRCgbsVKwCWtCbDjUaXeLJzcKEUSNsDtz1OIJGlyPGrV2ggEBADs=
"""
whitnode = """
R0lGODdhGgAYALMAAAAAAIAAAACAAICAAAAAgIAAgACAgICAgMDAwP8AAAD/AP//AAAA//8A/wD/
/////ywAAAAAGgAYAAMETPDJSau9eILMJfgg2FHh95TjKVoht2YmvFUzWdOxl2s7f90+FtDzG6p+
RSHG2NMhlVBaMkqa4oxCbNOa7Wxt25a3RE7xyFizes1eR2wAOw==
"""
yellow = """
R0lGODdhHgAeAPcAAP//AAAAAAAAMwAAZgAAmQAAzAAA/wAzAAAzMwAzZgAzmQAzzAAz/wBmAABm
MwBmZgBmmQBmzABm/wCZAACZMwCZZgCZmQCZzACZ/wDMAADMMwDMZgDMmQDMzADM/wD/AAD/MwD/
ZgD/mQD/zAD//zMAADMAMzMAZjMAmTMAzDMA/zMzADMzMzMzZjMzmTMzzDMz/zNmADNmMzNmZjNm
mTNmzDNm/zOZADOZMzOZZjOZmTOZzDOZ/zPMADPMMzPMZjPMmTPMzDPM/zP/ADP/MzP/ZjP/mTP/
zDP//2YAAGYAM2YAZmYAmWYAzGYA/2YzAGYzM2YzZmYzmWYzzGYz/2ZmAGZmM2ZmZmZmmWZmzGZm
/2aZAGaZM2aZZmaZmWaZzGaZ/2bMAGbMM2bMZmbMmWbMzGbM/2b/AGb/M2b/Zmb/mWb/zGb//5kA
AJkAM5kAZpkAmZkAzJkA/5kzAJkzM5kzZpkzmZkzzJkz/5lmAJlmM5lmZplmmZlmzJlm/5mZAJmZ
M5mZZpmZmZmZzJmZ/5nMAJnMM5nMZpnMmZnMzJnM/5n/AJn/M5n/Zpn/mZn/zJn//8wAAMwAM8wA
ZswAmcwAzMwA/8wzAMwzM8wzZswzmcwzzMwz/8xmAMxmM8xmZsxmmcxmzMxm/8yZAMyZM8yZZsyZ
mcyZzMyZ/8zMAMzMM8zMZszMmczMzMzM/8z/AMz/M8z/Zsz/mcz/zMz///8AAP8AM/8AZv8Amf8A
zP8A//8zAP8zM/8zZv8zmf8zzP8z//9mAP9mM/9mZv9mmf9mzP9m//+ZAP+ZM/+ZZv+Zmf+ZzP+Z
///MAP/MM//MZv/Mmf/MzP/M////AP//M///Zv//mf//zP///wAAAA0NDRoaGigoKDU1NUNDQ1BQ
UF1dXWtra3h4eIaGhpOTk6Ghoa6urru7u8nJydbW1uTk5PHx8f///wAAAAEBAQICAgMDAwQEBAUF
BQYGBgcHBwgICAkJCQoKCgsLCwwMDA0NDQ4ODg8PDxAQEBERERISEiwAAAAAHgAeAAcIMgABCBxI
sKDBgwgTKlzIsKHDhxAjSpxIsaLFixgzatzIsaPHjyBDihxJsqTJkyhTngwIADs=
"""
def Init():
import GatoUtil
imageCache = GatoUtil.ImageCache() # singleton
imageCache.AddImage("Icons/black.gif",black)
imageCache.AddImage("Icons/blacknde.gif",blacknde)
imageCache.AddImage("Icons/bledge.gif",bledge)
imageCache.AddImage("Icons/bluenode.gif",bluenode)
imageCache.AddImage("Icons/catbox_splash.gif",catbox_splash)
imageCache.AddImage("Icons/delete.gif",delete)
imageCache.AddImage("Icons/delete_1.gif",delete_1)
imageCache.AddImage("Icons/delete_2.gif",delete_2)
imageCache.AddImage("Icons/delete_3.gif",delete_3)
imageCache.AddImage("Icons/dkblnode.gif",dkblnode)
imageCache.AddImage("Icons/dkgnnode.gif",dkgnnode)
imageCache.AddImage("Icons/edge.gif",edge)
imageCache.AddImage("Icons/edge_1.gif",edge_1)
imageCache.AddImage("Icons/edge_2.gif",edge_2)
imageCache.AddImage("Icons/edge_3.gif",edge_3)
imageCache.AddImage("Icons/edit.gif",edit)
imageCache.AddImage("Icons/edit_1.gif",edit_1)
imageCache.AddImage("Icons/edit_2.gif",edit_2)
imageCache.AddImage("Icons/edit_3.gif",edit_3)
imageCache.AddImage("Icons/gato.gif",gato)
imageCache.AddImage("Icons/getgray.gif",getgray)
imageCache.AddImage("Icons/gnedge.gif",gnedge)
imageCache.AddImage("Icons/gred.gif",gred)
imageCache.AddImage("Icons/greennde.gif",greennde)
imageCache.AddImage("Icons/grey.gif",grey)
imageCache.AddImage("Icons/greynode.gif",greynode)
imageCache.AddImage("Icons/ltblnode.gif",ltblnode)
imageCache.AddImage("Icons/ltgnnode.gif",ltgnnode)
imageCache.AddImage("Icons/pink.gif",pink)
imageCache.AddImage("Icons/red.gif",red)
imageCache.AddImage("Icons/rednode.gif",rednode)
imageCache.AddImage("Icons/swap.gif",swap)
imageCache.AddImage("Icons/swap_1.gif",swap_1)
imageCache.AddImage("Icons/swap_2.gif",swap_2)
imageCache.AddImage("Icons/swap_3.gif",swap_3)
imageCache.AddImage("Icons/vertex.gif",vertex)
imageCache.AddImage("Icons/vertex_1.gif",vertex_1)
imageCache.AddImage("Icons/vertex_2.gif",vertex_2)
imageCache.AddImage("Icons/vertex_3.gif",vertex_3)
imageCache.AddImage("Icons/whitnode.gif",whitnode)
imageCache.AddImage("Icons/yellow.gif",yellow)
zAJkzM5kzZpkzmZkzzJkz/5lmAJlmM5lmZplmmZlmzJlm/5mZAJmZM5mZ
ZpmZmZmZzJmZ/5nMAJnMM5nMZpnMmZnMzJnM/5n/AJn/M5n/Zpn/mZn/zJn//8wAAMwAM8wAZswA
mcwAzMwA/8wzAMwzM8wzZswzmcwzzMwz/8xmAMxmM8xmZsxmmcxmzMxm/8yZAMyZM8yZZsyZmcyZ
zMyZ/8zMAMzMM8zMZszMmczMzMzM/8z/AMz/M8z/Zsz/mcz/zMz///8AAP8AM/8AZv8Amf8AzP8A
//8zAP8zM/8zZv8zmf8zzP8z//9mAP9mM/9mZv9mmf9mzP9m//+ZAP+ZM/+ZZv+Zmf+ZzP+Z///M
AP/MM//MZv/Mmf/MzP/M////AP//M///Zv//mf//zP///wAAAAAAAAAAGato/GatoSystemConfiguration.py 0100644 0017676 0017534 00000071656 10014153352 0020232 0 ustar 00schliep catbox 0000305 0050066 ################################################################################
#
# This file is part of Gato (Graph Animation Toolbox)
#
# file: GatoFile.py
# author: Achim Gaedke (achim.gaedke@zpr.uni-koeln.de)
#
# Copyright (C) 1998-2003, Alexander Schliep, Winfried Hochstaettler and
# ZAIK/ZPR, Universitaet zu Koeln
#
# Contact: schliep@molgen.mpg.de, wh@zpr.uni-koeln.de
# Information: http://gato.sf.net
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Library General Public
# License as published by the Free Software Foundation; either
# version 2 of the License, or (at your option) any later version.
#
# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Library General Public License for more details.
#
# You should have received a copy of the GNU Library General Public
# License along with this library; if not, write to the Free
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#
# This file is version $Revision: 1.3 $
# from $Date: 2003/02/12 14:24:58 $
# last change by $Author: schliep $.
#
################################################################################
import os
import os.path
import shutil
import sys
import string
import re
import Tkinter
import tkSimpleDialog
import tkFileDialog
import traceback
try:
import _winreg
except ImportError:
# we are not on a windows system
pass
gatoMimeType="application/gato"
gatoFileExtension="gato"
gatoDescription="gato graph/algorithm tool"
class ConfigurationException(Exception):
"""
to report configuration errors
"""
def __init__(self,message):
Exception.__init__(self)
self.message=message
def __repr__(self):
if hasattr(self,"message"):
return "%s: %s"%(self.__class__.__name__,self.message)
else:
return self.__class__.__name__
class configureOS:
"""
system configuration for gato file type support
"""
def __init__(self,DialogMaster=None):
"""
creates configurator object
"""
# find script location
self.myExecutable=os.path.abspath(sys.argv[0])
self.DialogMaster=DialogMaster
def check(self):
"""
check, if already configured, returns true, if this executable is installed
"""
raise ConfigurationException("base class method should not be called")
def askUserInstall(self):
"""
ask user, if she/he likes configuration
"""
raise ConfigurationException("base class method should not be called")
def askUserUninstall(self):
"""
ask user, if she/he likes uninstallation
"""
raise ConfigurationException("base class method should not be called")
def configureSystem(self):
"""
do system configuration
"""
raise ConfigurationException("base class method should not be called")
def unconfigureSystem(self):
"""
do system unconfiguration
"""
raise ConfigurationException("base class method should not be called")
def runInstall(self):
if not self.check() and self.askUserInstall():
self.configureSystem()
def runUninstall(self):
if self.check() and self.askUserUninstall():
self.unconfigureSystem()
def installBinary(self,path):
"""
installs this binary to another place
"""
# test, if it is our place...
if not os.path.exists(path):
os.makedirs(path)
newLocation=os.path.join(path,os.path.basename(self.myExecutable))
if os.path.exists(newLocation):
os.remove(newLocation)
shutil.copy2(self.myExecutable,newLocation)
self.myExecutable=os.path.abspath(newLocation)
class configureUnsupported(configureOS):
"""
some tips and exit
"""
check=configureSystem=askUserInstall=lambda x:None
def runInstall(self):
"""
"""
print "unsupported operating system %s"%sys.platform
class configureUNIX(configureOS):
"""
linux configuration
expands mailcap file
"""
def __init__(self,DialogMaster=None):
"""
find script location, configuration file location...
"""
configureOS.__init__(self,DialogMaster)
# find mailcap file
if not os.environ.has_key("HOME"):
raise ConfigurationException("could not determine home directory")
self.mailcapFile=os.path.join(os.environ["HOME"],".mailcap")
self.mime_typesFile=os.path.join(os.environ["HOME"],".mime.types")
def check(self):
"""
"""
self.cache_check_mailcap=self.check_mailcap()
self.cache_check_mime_types=self.check_mime_types()
return self.cache_check_mailcap and self.cache_check_mime_types
def check_mailcap(self):
"""
"""
if not os.access(self.mailcapFile,os.R_OK):
return 0
mailcap=file(self.mailcapFile,"r")
while 1:
line=mailcap.readline()
# line end
if not line:
break
# skip comments
if line[0]=="#":
continue
line=line.strip()
while line[-1]=="\\":
line=line[:-1]+mailcap.readline().rstrip()
entries=line.split(";")
(mimeType,viewCommand)=map(string.strip,entries[:2])
if (mimeType==gatoMimeType and
os.path.exists(viewCommand.split(" ")[0]) and
os.path.samefile(viewCommand.split(" ")[0],self.myExecutable)):
return 1
return 0
def check_mime_types(self):
"""
"""
if not os.access(self.mime_typesFile,os.R_OK):
return 0
mime_types=file(self.mime_typesFile,"r")
while 1:
line=mime_types.readline()
# line end
if not line:
break
# skip comments
if line[0]=="#":
continue
#catenate lines
line=line.strip()
while line[-1]=="\\":
newLine=mime_types.readline()
if newLine=="":
break
line=line[:-1]+newLine.rstrip()
mime_dict={}
# format: key=value or key="value"
kv_pair=re.compile("(\w+)=((?:\"[^\"]*\")|(?:[^\"\s]*))")
kv_pairs=kv_pair.findall(line)
for kv in kv_pairs:
(key,value)=kv
if value[0]=="\"":
value=value[1:-1]
mime_dict[key]=value
if mime_dict.get("type")=="application/gato" and mime_dict.get("exts")=="gato":
return 1
return 0
class askUserInstallDialog(tkSimpleDialog.Dialog):
"""
dialog for system file manipulation of linux systems
"""
def __init__(self,master,title=None):
if not title:
title="System Configuration"
tkSimpleDialog.Dialog.__init__(self,master,title)
def body(self, master):
"""
"""
row=0
# install prefix question
Tkinter.Label(master, text="Install Gato to another place?").grid(row=row,column=0)
self.installQ = Tkinter.IntVar()
self.installQ.set(1)
Tkinter.Radiobutton(master, text="Yes", variable=self.installQ, value=1,
command=self.enablePathEntry).grid(row=row,column=1)
Tkinter.Radiobutton(master, text="No", variable=self.installQ, value=2,
command=self.disablePathEntry).grid(row=row,column=2)
# install location
row+=1
self.installP=Tkinter.Entry(master)
self.installP.insert(0,os.path.expanduser("~/bin"))
self.installP.rowLocation=row
self.installP.colLocation=0
self.installP.grid(row=row,column=0,sticky=Tkinter.EW)
self.searchP=Tkinter.Button(master,text="search...",
command=self.askInstallPrefix,
pady=0)
self.searchP.grid(row=row,column=1,columnspan=2)
# mime type reg?
row+=1
Tkinter.Label(master, text="add gato mime type").grid(row=row,column=0)
self.mimeQ = Tkinter.IntVar()
self.mimeQ.set(1)
Tkinter.Radiobutton(master, text="Yes", variable=self.mimeQ,
value=1).grid(row=row,column=1)
Tkinter.Radiobutton(master, text="No", variable=self.mimeQ,
value=2).grid(row=row,column=2)
# mime type reg?
row+=1
Tkinter.Label(master, text="add gato to .mailcap").grid(row=row,column=0)
self.mailcapQ = Tkinter.IntVar()
self.mailcapQ.set(1)
Tkinter.Radiobutton(master, text="Yes", variable=self.mailcapQ,
value=1).grid(row=row,column=1)
Tkinter.Radiobutton(master, text="No", variable=self.mailcapQ,
value=2).grid(row=row,column=2)
def enablePathEntry(self):
self.installP.grid(row=self.installP.rowLocation,column=self.installP.colLocation,sticky=Tkinter.EW)
self.searchP["state"]=Tkinter.NORMAL
def disablePathEntry(self):
self.installP.grid_forget()
self.searchP["state"]=Tkinter.DISABLED
def askInstallPrefix(self):
"""
command for search button
"""
initDir=self.installP.get()
newPrefix=""
if os.path.exists(initDir):
newPrefix=tkFileDialog.askdirectory(parent=self,initialdir=initDir,mustexist=1)
else:
newPrefix=tkFileDialog.askdirectory(parent=self,mustexist=1)
if newPrefix:
self.installP.delete(0, Tkinter.END)
self.installP.insert(0, newPrefix)
def apply(self):
"""
return result as dictionary
"""
self.result={}
if (hasattr(self,"installP") and
hasattr(self,"installQ") and
(self.installQ.get()==1)):
self.result["installTo"]=self.installP.get()
else:
self.result["installTo"]=None
if hasattr(self,"mimeQ"):
self.result["mimeQ"]=self.mimeQ.get()==1
if hasattr(self,"mailcapQ"):
self.result["mailcapQ"]=self.mailcapQ.get()==1
def askUserInstall(self):
"""
"""
self.askedUser=self.askUserInstallDialog(self.DialogMaster).result
return self.askedUser
class askUserUninstallDialog(tkSimpleDialog.Dialog):
"""
dialog for system file manipulation of linux systems
"""
def __init__(self,master,title=None):
if not title:
title="System Configuration"
tkSimpleDialog.Dialog.__init__(self,master,title)
def body(self, master):
"""
Are you sure...? Dialog...
"""
row=0
# install prefix question
Tkinter.Label(master,
text="Install Gato from\n%s ?"%self.myExecutable
).grid(row=row,column=0)
def apply(self):
"""
return result as dictionary
"""
self.result={}
def askUserUninstall(self):
self.askedUser=self.askUserUnnstallDialog(self.DialogMaster).result
return self.askedUser
def configureSystem(self):
"""
"""
if self.askedUser.get("installTo",0):
self.installBinary(self.askedUser["installTo"])
if not self.cache_check_mailcap and self.askedUser.get("mailcapQ",0):
self.configure_mailcap()
if not self.cache_check_mime_types and self.askedUser.get("mimeQ",0):
self.configure_mime_types()
def configure_mailcap(self):
"""
append my mime type to .mailcap
"""
savedLines=""
# if this file exists
if os.access(self.mailcapFile,os.R_OK):
mailcap=file(self.mailcapFile,"r")
while 1:
line=mailcap.readline()
savedLine=line[:]
# line end
if not line:
break
# skip comments
if line[0]=="#":
savedLines+=line
continue
line=line.strip()
continuedLines=[]
while line[-1]=="\\":
continuedLines.append(savedLine)
savedLine=mailcap.readline()
line=line[:-1]+savedLine.rstrip()
entries=line.split(";")
(mimeType,viewCommand)=map(string.strip,entries[:2])
# skip my old entry
if mimeType==gatoMimeType:
continue
savedLines+=string.join(continuedLines,'')+savedLine
mailcap.close()
# open for write access
mailcap=file(self.mailcapFile,"w")
mailcap.write(savedLines)
mailcap.write("%s;%s \"%%s\"\n"%(gatoMimeType,self.myExecutable))
mailcap.close()
def configure_mime_types(self):
"""
append my information to .mime.types
"""
savedLines=""
# if this file exists
if os.access(self.mime_typesFile,os.R_OK):
mime_types=file(self.mime_typesFile,"r")
while 1:
line=mime_types.readline()
savedLine=line[:]
# line end
if not line:
break
# skip comments
if line[0]=="#":
savedLines+=line
continue
line=line.strip()
continuedLines=[]
while line[-1]=="\\":
continuedLines.append(savedLine)
savedLine=mime_types.readline()
line=line[:-1]+savedLine.rstrip()
entries=line.split(";")
mime_dict={}
# format: key=value or key="value"
kv_pair=re.compile("(\w+)=((?:\"[^\"]*\")|(?:[^\"\s]*))")
kv_pairs=kv_pair.findall(line)
for kv in kv_pairs:
(key,value)=kv
if value[0]=="\"":
value=value[1:-1]
mime_dict[key]=value
if (mime_dict.get("type")==gatoMimeType and
mime_dict.get("exts")==gatoFileExtension):
continue
savedLines+=string.join(continuedLines,'')+savedLine
mime_types.close()
else:
# fake Netscape MIME file
savedLines+="#--Netscape Communications Corporation MIME Information\n"
# open for write access
mime_types=file(self.mime_typesFile,"w")
mime_types.write(savedLines)
mime_types.write("# inserted by gato SystemConfiguration Module\n")
mime_types.write("type=%s \\\ndesc=\"%s\" \\\nexts=\"%s\"\n"%
(gatoMimeType,gatoDescription,gatoFileExtension))
mime_types.close()
class configureLinux(configureUNIX):
"""
"""
def __init__(self, DialogMaster=None):
if sys.platform[:5]!='linux':
raise ConfigurationException("tried to instantiate %s on %s"%
(self.__class__.__name__,sys.platform))
configureUNIX.__init__(self,DialogMaster)
class configureSUNOS(configureUNIX):
"""
"""
def __init__(self, DialogMaster=None):
if sys.platform[:5]!='sunos':
raise ConfigurationException("tried to instantiate %s on %s"%
(self.__class__.__name__,sys.platform))
configureUNIX.__init__(self,DialogMaster)
class configureWindows(configureOS):
"""
Configuration module for windows
contaminates the windows registry with our
extension, program and mime type
"""
def __init__(self,DialogMaster=None):
"""
find script location...
"""
configureOS.__init__(self,DialogMaster)
def check(self):
"""
"""
self.ClassesSection=self.findWritableClassesSection()
return 0
class askUserInstallDialog(tkSimpleDialog.Dialog):
"""
"""
def __init__(self,master,title=None):
if not title:
title="System Configuration"
tkSimpleDialog.Dialog.__init__(self,master,title)
def body(self, master):
"""
dialog body
"""
row=0
# install question
Tkinter.Label(master, text="Install Gato.exe to another place?").grid(row=row,column=0)
self.installQ = Tkinter.IntVar()
self.installQ.set(1)
Tkinter.Radiobutton(master, text="Yes", variable=self.installQ, value=1, command=self.enablePathEntry).grid(row=row,column=1)
Tkinter.Radiobutton(master, text="No", variable=self.installQ, value=2, command=self.disablePathEntry).grid(row=row,column=2)
# install location
row+=1
self.installP=Tkinter.Entry(master)
self.installP.insert(0,"C:\\Gato\\")
self.installP.rowLocation=row
self.installP.colLocation=0
self.installP.grid(row=row,column=0,sticky=Tkinter.EW)
self.searchP=Tkinter.Button(master,text="search...",command=self.askInstallPrefix, pady=0)
self.searchP.grid(row=row,column=1,columnspan=2)
# extension question
row+=1
self.extensionQ = Tkinter.IntVar()
self.extensionQ.set(1)
Tkinter.Label(master, text="Create bindings to .%s file extensions:"%gatoFileExtension).grid(row=row)
Tkinter.Radiobutton(master, text="Yes", variable=self.extensionQ, value=1).grid(row=row,column=1)
Tkinter.Radiobutton(master, text="No", variable=self.extensionQ, value=2).grid(row=row,column=2)
# MIME type question
row+=1
self.mimeQ = Tkinter.IntVar()
self.mimeQ.set(1)
Tkinter.Label(master, text="Register MIME type %s:"%gatoMimeType).grid(row=row)
Tkinter.Radiobutton(master, text="Yes", variable=self.mimeQ, value=1).grid(row=row,column=1)
Tkinter.Radiobutton(master, text="No", variable=self.mimeQ, value=2).grid(row=row,column=2)
def apply(self):
"""
return result as dictionary
"""
self.result={}
if (hasattr(self,"installP") and
hasattr(self,"installQ") and
(self.installQ.get()==1)):
self.result["installTo"]=self.installP.get()
else:
self.result["installTo"]=None
if hasattr(self,"mimeQ"):
self.result["mimeQ"]=self.mimeQ.get()==1
if hasattr(self,"extensionsQ"):
self.result["extensionsQ"]=self.extensionsQ.get()==1
def enablePathEntry(self):
self.installP.grid(row=self.installP.rowLocation,column=self.installP.colLocation,sticky=Tkinter.EW)
self.searchP["state"]=Tkinter.NORMAL
def disablePathEntry(self):
self.installP.grid_forget()
self.searchP["state"]=Tkinter.DISABLED
def askInstallPrefix(self):
"""
command for search button
"""
initDir=self.installP.get()
newPrefix=""
if os.path.exists(initDir):
newPrefix=tkFileDialog.askdirectory(parent=self,initialdir=initDir,mustexist=1)
else:
newPrefix=tkFileDialog.askdirectory(parent=self,mustexist=1)
if newPrefix:
self.installP.delete(0, Tkinter.END)
self.installP.insert(0, newPrefix)
def askUserInstall(self):
"""
"""
self.askedUser=self.askUserInstallDialog(self.DialogMaster).result
return self.askedUser
def configureSystem(self):
"""
do the system configuration
"""
if self.askedUser.get("installTo",0):
self.installBinary(self.askedUser["installTo"])
if self.askedUser.get("extensionQ",0) or self.askedUser.get("mimeQ",0):
self.insertGatoEntries(self.ClassesSection)
def printGatoEntries(self):
"""
start reading...
"""
reader=_winreg.ConnectRegistry(None,_winreg.HKEY_CLASSES_ROOT)
# get Gato.File section
GatoFileHandle=None
try:
GatoFileHandle=_winreg.OpenKey(reader,"Gato.File")
except WindowsError:
print "Could not find Gato.File section"
else:
print "found Gato.File section:"
self.printSubRegistry(GatoFileHandle)
# get Gato FileExtension Section
GatoExtensionHandle=None
try:
GatoExtensionHandle=_winreg.OpenKey(reader,"."+gatoFileExtension)
except WindowsError:
print "could not find the file extension .%s"%gatoFileExtension
else:
print "found gato's extension"
self.printSubRegistry(GatoExtensionHandle)
# get Gato mime Type section
GatoMimeHandleGatoExt=None
try:
GatoMimeHandle=_winreg.OpenKey(reader,"MIME")
GatoMimeHandleDatabase=_winreg.OpenKey(GatoMimeHandle,
"Database")
GatoMimeHandleContentType=_winreg.OpenKey(GatoMimeHandleDatabase,
"Content Type")
GatoMimeHandleGatoExt=_winreg.OpenKey(GatoMimeHandleContentType,
gatoMimeType)
except WindowsError:
print "could not find mime type: %s"%gatoMimeType
else:
print "found %s mime type"%gatoMimeType
self.printSubRegistry(GatoMimeHandleGatoExt)
def printSubRegistry(self,key,indent=""):
"""
print all information of a subkey
"""
subkeyNo,valueNo,lastMod=_winreg.QueryInfoKey(key)
for i in range(valueNo):
print indent,_winreg.EnumValue(key, i)
for i in range(subkeyNo):
subkeyName=_winreg.EnumKey(key,i)
subkey=_winreg.OpenKey(key,subkeyName)
print indent,subkeyName
self.printSubRegistry(subkey,indent+" ")
def findWritableClassesSection(self):
# first try in HKEY_CLASSES_ROOT
try:
writer=_winreg.ConnectRegistry(None,_winreg.HKEY_CLASSES_ROOT)
GatoFileTestHandle=_winreg.CreateKey(writer,"Gato.File.Test")
_winreg.DeleteKey(writer,"Gato.File.Test")
return writer
except WindowsError:
# print "could not access HKEY_CLASSES_ROOT/Gato.File"
# self.traceback.print_exc()
pass
# next try...
try:
writer=_winreg.ConnectRegistry(None,_winreg.HKEY_CURRENT_USER)
SoftwareSection=_winreg.OpenKey(writer,"Software")
ClassesSection=_winreg.OpenKey(SoftwareSection,"Classes",0,_winreg.KEY_SET_VALUE)
GatoFileTestHandle=_winreg.CreateKey(ClassesSection,"Gato.File.Test")
_winreg.DeleteKey(ClassesSection,"Gato.File.Test")
return ClassesSection
except WindowsError:
# print "could not access HKEY_CURRENT_USER/Software/Classes Section"
# self.traceback.print_exc()
return None
def insertGatoEntries(self,ClassesSection):
"""
updates registry database
"""
# update Gato.File section
try:
GatoFileHandle=_winreg.CreateKey(ClassesSection,"Gato.File")
_winreg.SetValueEx(GatoFileHandle,"",0,_winreg.REG_SZ,"Gato.File")
except WindowsError:
print "Could not create/update the Gato.File section"
self.traceback.print_exc()
# update Gato.File's subsections
try:
GatoShellHandle=_winreg.CreateKey(GatoFileHandle,"shell")
GatoOpenHandle=_winreg.CreateKey(GatoShellHandle,"open")
GatoOpenCommandHandle=_winreg.CreateKey(GatoOpenHandle,"command")
_winreg.SetValueEx(GatoOpenCommandHandle,"",0,_winreg.REG_SZ,self.myExecutable+' "%1"')
except WindowsError:
print "could not install open command for gato"
self.traceback.print_exc()
# update .gato section
try:
GatoExtensionHandle=_winreg.CreateKey(ClassesSection,"."+gatoFileExtension)
_winreg.SetValueEx(GatoExtensionHandle,"",0,_winreg.REG_SZ,"Gato.File")
_winreg.SetValueEx(GatoExtensionHandle,"Content Type",0,_winreg.REG_SZ,gatoMimeType)
except WindowsError:
print "could not create/update FileExtension section"
self.traceback.print_exc()
# access MIME Database section
MimeContentTypeSection=None
try:
MimeSection=_winreg.CreateKey(ClassesSection,"MIME")
MimeDatabaseSection=_winreg.CreateKey(MimeSection,"Database")
MimeContentTypeSection=_winreg.CreateKey(MimeDatabaseSection,"Content Type")
except WindowsError:
print "could not access MIME Content Type database"
self.traceback.print_exc()
return
# update gato's mime type
try:
GatoMimeContentTypeHandle=_winreg.CreateKey(MimeContentTypeSection,gatoMimeType)
_winreg.SetValueEx(GatoMimeContentTypeHandle,"Extension",0,
_winreg.REG_SZ,"."+gatoFileExtension)
except WindowsError:
print "could not update the gato MIME section"
self.traceback.print_exc()
class GatoInstaller:
"""
A instance of this class reflects the installation capabilities and status.
It should be instantiated only once.
"""
instanceCounter=0
def __init__(self,disabled=0,menu=None,index=Tkinter.END):
"""
gets system configurator and checks state
"""
# assure singleton
if GatoInstaller.instanceCounter!=0:
raise ConfigurationException("class GatoInstaller should be instantiated once.")
GatoInstaller.instanceCounter+=1
self.disabled=disabled
self.menu=menu
self.index=index
# set if no menu item should appear
if self.disabled:
return
self.SysConfig=self.getConfigurator()
self.state=self.SysConfig.check()
def __del__(self):
"""
clean up and decrement
"""
GatoInstaller.instanceCounter-=1
def enable(self):
self.disabled=0
self.SysConfig=getConfigurator()
self.state=self.SysConfig.check()
if self.menu is not None:
self.insertMenuEntry()
def disable(self):
self.disabled=1
self.removeMenuEntry()
def addMenuEntry(self,menu,index=Tkinter.END):
self.menu=menu
self.index=menu.index(index) # make absolute position
if self.disabled:
return
self.insertMenuEntry()
def insertMenuEntry(self):
if self.menu is None:
return
self.menu.insert_command(self.index)
self.configureMenuEntry()
def removeMenuEntry(self):
if self.menu is None:
return
self.menu.delete(self.index)
def configureMenuEntry(self):
if self.menu is None:
return
if self.state:
self.menu.entryconfig(self.index,
command=self.uninstallCommand,
label='Uninstall Gato'
)
else:
self.menu.entryconfig(self.index,
command=self.installCommand,
label='Install Gato...')
def installCommand(self):
self.SysConfig.runInstall()
self.state=self.SysConfig.check()
self.configureMenuEntry()
def uninstallCommand(self):
print "uninstall"
self.state=self.SysConfig.check()
self.configureMenuEntry()
def getConfigurator(self):
if sys.platform[:5]=="linux":
return configureLinux()
elif sys.platform=="win32":
return configureWindows()
elif sys.platform[:5]=="sunos":
return configureSUNOS()
else:
return configureUnsupported()
if __name__=="__main__":
i=GatoInstaller()
return result as dictionary
"""
self.result={}
Gato/GatoTest.py 0100644 0017676 0017534 00000005065 10014153352 0015124 0 ustar 00schliep catbox 0000305 0050066 #!/usr/bin/env python
################################################################################
#
# This file is part of Gato (Graph Animation Toolbox)
#
# file: GatoTest.py
# author: Alexander Schliep (schliep@molgen.mpg.de)
#
# Copyright (C) 1998-2003, Alexander Schliep, Winfried Hochstaettler and
# ZAIK/ZPR, Universitaet zu Koeln
#
# Contact: schliep@molgen.mpg.de, wh@zpr.uni-koeln.de
#
# Information: http://gato.sf.net
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Library General Public
# License as published by the Free Software Foundation; either
# version 2 of the License, or (at your option) any later version.
#
# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Library General Public License for more details.
#
# You should have received a copy of the GNU Library General Public
# License along with this library; if not, write to the Free
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#
#
#
# This file is version $Revision: 1.12 $
# from $Date: 2003/02/12 14:24:58 $
# last change by $Author: schliep $.
#
################################################################################
from Gato import *
testPath = "../CATBox/"
tests = [ ("04-MinimalSpanningTrees/Prim.alg",
"04-MinimalSpanningTrees/MinimalSpanningTrees08.cat"),
("04-MinimalSpanningTrees/Kruskal.alg",
"04-MinimalSpanningTrees/MinimalSpanningTrees08.cat"),
("06-MaximalFlows/FordFulkerson.alg",
"06-MaximalFlows/FordFulkerson3.cat"),
("06-MaximalFlows/PreflowPush.alg",
"06-MaximalFlows/PreflowPush2.cat")
]
if __name__ == '__main__':
app = AlgoWin()
app.algorithm.logAnimator=1
globals()['gInteractive'] = 0
print "GatoTest",globals()['gInteractive']
for case in tests:
print "=== TEST ===",case[0],"===",case[1],"==="
app.OpenAlgorithm(testPath + case[0])
globals()['gInteractive'] = 0
app.algorithm.ClearBreakpoints()
app.update_idletasks()
app.update()
app.OpenGraph(testPath + case[1])
app.update_idletasks()
app.update()
# Run it ...
app.after_idle(app.CmdContinue) # after idle needed since CmdStart
# does not return
app.CmdStart()
app.update_idletasks()
#app.mainloop()
Gato/GatoUtil.py 0100644 0017676 0017534 00000007773 10014153352 0015132 0 ustar 00schliep catbox 0000305 0050066 ################################################################################
#
# This file is part of Gato (Graph Animation Toolbox)
#
# file: GatoUtil.py
# author: Alexander Schliep (schliep@molgen.mpg.de)
#
# Copyright (C) 1998-2003, Alexander Schliep, Winfried Hochstaettler and
# ZAIK/ZPR, Universitaet zu Koeln
#
# Contact: schliep@molgen.mpg.de, wh@zpr.uni-koeln.de
#
# Information: http://gato.sf.net
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Library General Public
# License as published by the Free Software Foundation; either
# version 2 of the License, or (at your option) any later version.
#
# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Library General Public License for more details.
#
# You should have received a copy of the GNU Library General Public
# License along with this library; if not, write to the Free
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#
#
#
# This file is version $Revision: 1.12 $
# from $Date: 2003/09/13 21:27:59 $
# last change by $Author: schliep $.
#
################################################################################
from Tkinter import *
def gatoPath():
""" Returns the path to the directory containint Gato.py or Gred.py """
import os
return os.path.dirname(__name__ == '__main__' and sys.argv[0] or __file__)
def extension(pathAndFile):
""" Return ext if path/filename.ext is given """
import string
return string.split(stripPath(pathAndFile),".")[-1]
def stripPath(pathAndFile):
""" Return filename.ext if path/filename.ext is given """
import os
return os.path.split(pathAndFile)[1]
def orthogonal(u):
""" Return a unit length vector (v1,v2) which has an angle of
90 degrees clockwise to the vector u = (u1,u2) """
from math import sqrt
(u1,u2) = u
length = sqrt(u1**2 + u2**2)
if length < 0.001:
length = 0.001
u1 = u1 / length
u2 = u2 / length
return (-u2,u1)
def ArgMin(list,val):
""" Returns the element e of list for which val[e] is minimal """
if len(list) > 0:
min = val[list[0]]
minElem = list[0]
for e in list:
if val[e] < min:
min = val[e]
minElem = e
return minElem
def ArgMax(list,val):
""" Returns the element e of list for which val[e] is maximal """
if len(list) > 0:
max = val[list[0]]
maxElem = list[0]
for e in list:
if val[e] > max:
max = val[e]
maxElem = e
return maxElem
class MethodLogger:
""" Provide logging of method calls with parameters
E.g., for regression testing
XXX specify output channel (or do it via redirect ?)
"""
def __init__(self, object):
self.object = object
def __getattr__(self,arg):
self.methodName = arg
self.method = getattr(self.object,arg)
return getattr(self,'caller')
def caller(self,*args):
print self.methodName,"(",args,")"
return apply(self.method,args)
class ImageCache:
""" Provides a global cache for PhotoImages displayed in the
application. Singleton Pattern """
images = None
def __init__(self):
if ImageCache.images == None:
ImageCache.images = {}
def __getitem__(self, relURL):
""" Given a relative URL to an image file return the
corresponding PhotoImage. """
try:
if relURL not in self.images.keys():
ImageCache.images[relURL] = PhotoImage(file=relURL)
return ImageCache.images[relURL]
except IndexError, IOError:
import logging
log = logging.getLogger("GatoUtil.py")
log.exception("Error finding image %s" % relURL)
def AddImage(self, relURL, imageData):
ImageCache.images[relURL] = PhotoImage(data=imageData)
Gato/Graph.py 0100644 0017676 0017534 00000034206 10014153353 0014433 0 ustar 00schliep catbox 0000305 0050066 ################################################################################
#
# This file is part of Gato (Graph Animation Toolbox)
#
# file: Graph.py
# author: Alexander Schliep (schliep@molgen.mpg.de)
#
# Copyright (C) 1998-2003, Alexander Schliep, Winfried Hochstaettler and
# ZAIK/ZPR, Universitaet zu Koeln
#
# Contact: schliep@molgen.mpg.de, wh@zpr.uni-koeln.de
#
# Information: http://gato.sf.net
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Library General Public
# License as published by the Free Software Foundation; either
# version 2 of the License, or (at your option) any later version.
#
# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Library General Public License for more details.
#
# You should have received a copy of the GNU Library General Public
# License along with this library; if not, write to the Free
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#
#
#
# This file is version $Revision: 1.20 $
# from $Date: 2003/11/28 19:51:14 $
# last change by $Author: schliep $.
#
################################################################################
from GatoGlobals import *
from DataStructures import Point2D, VertexLabeling, EdgeLabeling, EdgeWeight
#from math import log
import logging
log = logging.getLogger("Graph.py")
################################################################################
#
# Graph
#
################################################################################
class Graph:
""" Provides a mathematical graph object consisting of vertices
and (directed) edges connecting those vertices. Graphs have
- a labeling for vertices allowing to specify names
- an embedding of vertices into 2D-space
- one or more sets of edge weights
Vertices are specified via id (integer number) and edges via
(tail,head)-tuples
NOTE: ids are supposed to be consecutive and ranging from 0
to G.Order() - 1 !!! Use the labeling to *display* other numbers
for vertices.
At least one set of edge weights is assumed to exist and accessible
as self.edgeWeights[0]; self.euclidian and Euclidify refer to this
self.edgeWeights[0]
"""
def __init__(self):
self.simple = 1
self.euclidian = 1
self.directed = 0
self.vertices = []
self.adjLists = {}
self.invAdjLists = {} # Inverse Adjazenzlisten
self.highVertexID = 0 # INTERNAL
self.embedding = VertexLabeling() # 2D-Positions
self.labeling = VertexLabeling() # Names of vertices
self.edgeWeights = {} # Dictionary of edge labellings
self.edgeWeights[0] = EdgeWeight(self)
self.vertexWeights = {} # None by default
self.size = 0
self.edgeWidth = None
self.vertexAnnotation = None
self.edgeAnnotation = None
self.properties = {}
def AddVertex(self):
""" Add an isolated vertex. Returns the id of the new vertex """
id = self.GetNextVertexID()
self.vertices.append(id)
self.adjLists[id] = []
self.invAdjLists[id] = []
return id
def DeleteVertex(self, v):
""" Delete the vertex v and its incident edges """
outVertices = self.OutNeighbors(v)[:] # Need a copy here
inVertices = self.InNeighbors(v)[:]
for w in outVertices:
self.DeleteEdge(v,w)
for w in inVertices:
if w != v: # We have already deleted loops
self.DeleteEdge(w,v)
self.vertices.remove(v)
#self.adjLists[v] = None
#self.invAdjLists[v] = None
# XXX Should clean up all other stuff too ...
def QVertex(self, v):
""" Check whether v is a vertex """
return v in self.vertices
def AddEdge(self,tail,head):
""" Add an edge (tail,head). Returns nothing
Raises GraphNotSimpleError if
- trying to add a loop
- trying to add an edge multiply
In case of directed graphs (tail,head) and (head,tail)
are distinct edges """
if self.simple == 1 and tail == head: # Loop
raise GraphNotSimpleError
if self.directed == 0 and tail in self.adjLists[head]:
raise GraphNotSimpleError
if head in self.adjLists[tail]: # Multiple edge
raise GraphNotSimpleError
self.adjLists[tail].append(head)
self.invAdjLists[head].append(tail)
self.size = self.size + 1
def DeleteEdge(self,tail,head):
""" Deletes edge (tail,head). Does *not* handle undirected graphs
implicitely. Raises NoSuchEdgeError upon error. """
try:
self.adjLists[tail].remove(head)
self.invAdjLists[head].remove(tail)
self.size = self.size - 1
except KeyError:
raise NoSuchEdgeError
def Edge(self,tail,head):
""" Handles undirected graphs by return correct ordered
vertices as (tail,head). Raises NoSuchEdgeError upon error. """
try:
if head in self.adjLists[tail]:
return (tail,head)
if self.directed == 0 and tail in self.adjLists[head]:
return (head,tail)
except KeyError:
raise NoSuchEdgeError
def QEdge(self,tail,head):
""" Returns 1 if (tail,head) is an edge in G. If G is undirected
order of vertices does not matter """
if self.directed == 1:
return head in self.adjLists[tail]
else:
return (head in self.adjLists[tail]) or (tail in self.adjLists[head])
def Neighborhood(self,v):
""" Returns the vertices which are connected to v. Does handle
undirected graphs (i.e., returns vertices w s.t. either
(v,w) or (w,v) is an edge) """
if self.directed:
return self.OutNeighbors(v)
else:
return self.InOutNeighbors(v)
def InNeighbors(self,v):
""" Returns vertices w for which (w,v) is an edge """
return self.invAdjLists[v]
def OutNeighbors(self,v):
""" Returns vertices w for which (v,w) is an edge """
return self.adjLists[v]
def InOutNeighbors(self,v):
""" Returns vertices w for which (v,w) or (w,v) is an edge """
return self.InNeighbors(v) + self.OutNeighbors(v)
def InEdges(self,v):
""" Returns edges (*,v) """
f = lambda x, vertex = v : (x,vertex)
return map(f, self.invAdjLists[v])
def OutEdges(self,v):
""" Returns edges (v,*) """
f = lambda x, vertex = v : (vertex,x)
return map(f ,self.adjLists[v])
def IncidentEdges(self,v):
""" Returns edges (v,*) and (*,v) """
return self.InEdges(v) + self.OutEdges(v)
def Edges(self):
""" Returns all edges """
tmp = []
for v in self.vertices:
tmp = tmp + self.OutEdges(v)
return tmp
def Vertices(self):
""" Returns all edges """
return self.vertices
def printMy(self):
""" Debugging only """
for v in self.vertices:
print v, " -- ", self.adjLists[v]
def GetNextVertexID(self):
""" *Internal* returns next free vertex id """
self.highVertexID = self.highVertexID + 1
return self.highVertexID
def Order(self):
""" Returns order i.e., the number of vertices """
return len(self.vertices)
def Size(self):
""" Returns size i.e., the number of edge """
return self.size
def Degree(self, v):
""" Returns the degree of the vertex v, which is
- the number of incident edges in the undirect case
- the number of outgoing edges in the directed case """
if self.directed:
return len(self.adjLists[v])
else:
return len(self.adjLists[v]) + len(self.invAdjLists[v])
def InDegree(self, v):
""" Returns the number of incoming edges for direct graphs """
if self.directed:
return len(self.invAdjLists[v])
else:
return None # Proper error to raise?
def OutDegree(self, v):
""" Returns the number of incoming edges for direct graphs """
if self.directed:
return len(self.adjLists[v])
else:
return None # Proper error to raise?
def QEuclidian(self):
""" Returns 1 if the graph is euclidian, 0 else """
return self.euclidian
def QDirected(self):
""" Returns 1 if the graph is directed, 0 else """
return self.directed
def CalculateWidthFromWeight(self, scale, weightID = 0):
""" Calculate width of edges (self.edgeWidth will be used by
GraphDisplay if not none) from the specified set of edge
weights.
Default: weightID = 0 is used """
self.edgeWidth = EdgeLabeling()
edges = self.Edges()
maxWeight = max(self.edgeWeights[weightID].label.values())
for e in edges:
self.edgeWidth[e] = scale * (1 + 35 * self.edgeWeights[weightID][e] / maxWeight)
def NrOfEdgeWeights(self):
return len(self.edgeWeights.keys())
def NrOfVertexWeights(self):
return len(self.vertexWeights.keys())
def Euclidify(self):
""" Replace edge weights with weightID = 0 with Euclidean distance
between incident vertices """
for v in self.vertices:
for w in self.adjLists[v]:
d = ((self.embedding[v].x - self.embedding[w].x)**2 +
(self.embedding[v].y - self.embedding[w].y)**2)**(.5)
if self.edgeWeights[0].QInteger():
self.edgeWeights[0][(v,w)] = int(round(d))
else:
self.edgeWeights[0][(v,w)] = d
self.euclidian = 1
def Integerize(self, weightID = 0):
""" Integerize: Make all edge weights integers """
if weightID == 'all':
for w in self.edgeWeights.keys():
self.edgeWeights[w].Integerize()
else:
self.edgeWeights[weightID].Integerize()
def Undirect(self):
""" If (u,v) and (v,u) are edges in the directed graph, remove one of them.
to make graph undirected (no multiple edges allowed). Which one gets
deleted depends on ordering in adjacency lists. """
if not self.directed:
return
for v in self.vertices:
for w in self.adjLists[v]:
if v in self.adjLists[w]:
self.DeleteEdge(w,v)
self.directed = 0
def SetProperty(self, name, val):
""" Set the value of property 'name' to 'val' """
self.properties[name] = val
def Property(self,name):
""" Return the value of property 'name'. If the property
'name' has not been set 'Unknown' is returned """
try:
return self.properties[name]
except:
return 'Unknown'
def About(self):
""" Return string containing HTML code providing information
about the graph """
return " No information available
"
################################################################################
#
# Induced Subgraph
#
################################################################################
class SubGraph(Graph):
""" Provides a subgraph, i.e., a subset of the vertices and edges
of a specified graph
Vertices are specified via ids from its supergraph and edges via
(tail,head)-tuples
It also keeps track of the subgraphs total weight (= sum of edge
weights) for weights with weightID == 0
"""
def __init__(self,G):
Graph.__init__(self)
self.superGraph = G
self.embedding = self.superGraph.embedding
self.labeling = self.superGraph.labeling
self.edgeWeights = self.superGraph.edgeWeights
self.directed = self.superGraph.directed
self.totalWeight = 0
def AddVertex(self,v):
""" Add a vertex from the supergraph to the subgraph.
Returns NoSuchVertexError if v does not exist in
supergraph """
try:
self.vertices.append(v)
#f = lambda x, vertexList=self.vertices: x in vertexList
#self.adjLists[v] = filter(f, self.superGraph.adjLists[v])
#self.invAdjLists[v] = filter(f, self.superGraph.invAdjLists[v])
self.adjLists[v] = []
self.invAdjLists[v] = []
except:
raise NoSuchVertexError
def AddEdge(self,tail,head):
""" Add an edge from the supergraph to the subgraph.
Will also add tail and/or head if there are not
already in subgraph """
try:
if not tail in self.vertices:
self.AddVertex(tail)
if not head in self.vertices:
self.AddVertex(head)
(tail,head) = self.superGraph.Edge(tail,head)
self.adjLists[tail].append(head)
self.invAdjLists[head].append(tail)
self.size = self.size + 1
try:
w = self.superGraph.edgeWeights[0][(tail,head)]
except KeyError:
w = 0.0 # XXX we dont have w weight for the edge. Make totalWeight configurable/subclass
self.totalWeight += w
except (KeyError, NoSuchVertexError, NoSuchEdgeError):
raise NoSuchEdgeError
def AddSubGraph(self,G):
""" Add subgraph G to self. Will do nothing if self and G
have distinct supergraphs """
if self.superGraph != G.superGraph:
log.error("AddSubGraph: distinct superGraphs")
return
for v in G.vertices:
self.AddVertex(v)
for e in G.Edges():
self.AddEdge(e[0],e[1])
def DeleteEdge(self,tail,head):
""" Delete edge from subgraph. Raises NoSuchEdgeError
upon error """
if tail in self.vertices and head in self.vertices:
self.totalWeight = self.totalWeight - self.superGraph.edgeWeights[0][(tail,head)]
self.adjLists[tail].remove(head)
self.invAdjLists[head].remove(tail)
self.size = self.size - 1
else:
raise NoSuchEdgeError
def Clear(self):
""" Delete all vertices and edges from the subgraph. """
self.vertices = []
self.adjLists = {}
self.invAdjLists = {} # Inverse Adjazenzlisten
self.size = 0
self.totalWeight = 0
def GetNextVertexID(self):
""" *Internal* safeguard """
log.error("Induced Subgraph -> GetNextVertexID should never have been called")
def Weight(self):
""" Returns the total weight (= sum of edge weights) of subgraph """
return self.totalWeight
def QEuclidian(self):
""" Returns 1 if the super graph is euclidian, 0 else """
return self.superGraph.euclidian
def QDirected(self):
""" Returns 1 if the super graph is directed, 0 else"""
return self.superGraph.directed
def QEdge(self,tail,head):
""" Returns 1 if (tail,head) is an edge in G """
if not tail in self.vertices or not head in self.vertices:
return 0
if self.directed == 1:
return head in self.adjLists[tail]
else:
return head in self.adjLists[tail] or tail in self.adjLists[head]
of directed graphs (tail,head) and (head,tail)
are distinct edges """
if self.simple == 1 and tail == head: # Loop
raise GraphNotSimpleError
if self.directed == 0 and tail in self.adjLists[head]:
raise GraphNotSimpleError
if head in self.adjLists[tail]: # Multiple edge
raise GraphNotSimpleError
self.adjLists[tail].append(head)
self.invAdjListsGato/GraphCreator.py 0100644 0017676 0017534 00000054521 10014153353 0015755 0 ustar 00schliep catbox 0000305 0050066 ################################################################################
#
# This file is part of Gato (Graph Animation Toolbox)
#
# file: GraphCreator.py
# author: Ramazan Buzdemir (buzdemir@zpr.uni-koeln.de)
#
# Copyright (C) 1998-2003, Alexander Schliep, Winfried Hochstaettler and
# ZAIK/ZPR, Universitaet zu Koeln
#
# Contact: schliep@molgen.mpg.de, wh@zpr.uni-koeln.de
#
# Information: http://gato.sf.net
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Library General Public
# License as published by the Free Software Foundation; either
# version 2 of the License, or (at your option) any later version.
#
# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Library General Public License for more details.
#
# You should have received a copy of the GNU Library General Public
# License along with this library; if not, write to the Free
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#
#
#
# This file is version $Revision: 1.15 $
# from $Date: 2003/05/04 12:06:45 $
# last change by $Author: schliep $.
#
################################################################################
from Graph import *
from Embedder import *
import whrandom
class Creator:
""" This class provides an abstract Creator as
a base for actual Creator implementations """
def Name(self):
""" Return a short descriptive name for the creator e.g. usable as
a menu item """
return "none"
def Create(self, theGraphEditor):
""" Create a new graph. """
return none
def DrawNewGraph(theGraphEditor,G,direction):
theGraphEditor.NewGraph(direction,1,0,'None',0,'One',0)
for v in G.vertices:
theGraphEditor.AddVertex(G.xCoord[v],G.yCoord[v])
for e in G.Edges():
theGraphEditor.AddEdge(e[0],e[1])
#----------------------------------------------------------------------
from Tkinter import *
import tkSimpleDialog
import string
from tkMessageBox import showwarning
class Dialog(tkSimpleDialog.Dialog):
def __init__(self, master, planar, visible, Text):
self.planar=planar
self.visible=visible
tkSimpleDialog.Dialog.__init__(self, master, Text)
def body(self, master):
self.resizable(0,0)
self.number_of_nodes=StringVar()
self.number_of_nodes.set("1")
label = Label(master, text="number of nodes :", anchor=W)
label.grid(row=0, column=0, padx=0, pady=2, sticky="w")
entry=Entry(master, width=6, exportselection=FALSE,
textvariable=self.number_of_nodes)
entry.selection_range(0,1)
entry.focus_set()
entry.grid(row=0,column=1, padx=2, pady=2, sticky="w")
self.number_of_edges=StringVar()
self.number_of_edges.set("0")
if self.visible:
label = Label(master, text="number of edges :", anchor=W)
label.grid(row=1, column=0, padx=0, pady=2, sticky="w")
entry=Entry(master, width=6, exportselection=FALSE,
textvariable=self.number_of_edges)
entry.selection_range(0,1)
entry.focus_set()
entry.grid(row=1,column=1, padx=2, pady=2, sticky="w")
self.direction=IntVar()
self.direction.set(0)
radio=Radiobutton(master, text="Undirected", variable=self.direction,
value=0)
radio.grid(row=0, column=2, padx=2, pady=2, sticky="w")
radio=Radiobutton(master, text="Directed", variable=self.direction,
value=1)
radio.grid(row=1, column=2, padx=2, pady=2, sticky="w")
label = Label(master, text=" ")
label.grid(row=0, column=3, padx=5, pady=2)
self.layout=IntVar()
self.layout.set(0)
radio=Radiobutton(master, text="Randomize Layout",
variable=self.layout, value=0,
width=23, indicatoron=0, selectcolor="white")
radio.grid(row=0, column=4, padx=3, pady=2, sticky="w")
radio=Radiobutton(master, text="Circular Layout",
variable=self.layout, value=1,
width=23, indicatoron=0, selectcolor="white")
radio.grid(row=1, column=4, padx=3, pady=2, sticky="w")
if self.planar:
radio=Radiobutton(master, text="Planar Layout (FPP)",
variable=self.layout, value=2,
width=23, indicatoron=0, selectcolor="white")
radio.grid(row=3, column=4, padx=3, pady=2, sticky="w")
radio=Radiobutton(master, text="Planar Layout (Schnyder)",
variable=self.layout, value=3,
width=23, indicatoron=0, selectcolor="white")
radio.grid(row=4, column=4, padx=3, pady=2, sticky="w")
def validate(self):
try:
n=string.atoi(self.number_of_nodes.get())
if n<1 or n>200:
raise nodeError
except:
showwarning("Please try again !",
"min. number of nodes = 1\n"
"max. number of nodes = 200")
return 0
try:
m=string.atoi(self.number_of_edges.get())
if self.planar:
if n==1: max_m=0
else: max_m=6*n-12
else:
max_m=n*n-n
if self.direction.get()==0:
max_m = max_m/2
if m<0 or m>max_m:
raise edgeError
except:
showwarning("Please try again !",
"min. number of edges = 0\n"
"max. number of edges = %i" %max_m)
return 0
self.result=[]
self.result.append(n)
self.result.append(m)
self.result.append(self.direction.get())
self.result.append(self.layout.get())
return self.result
#----------------------------------------------------------------------
def CompleteEdges(G,n,direction):
Edges=[]
for i in range(0,n):
source=G.vertices[i]
for j in range(i+1,n):
target=G.vertices[j]
Edges.append((source,target))
if direction==1: Edges.append((target,source))
return Edges
def MaximalPlanarEdges(G,n,direction):
Edges=[] #6*n
AdjEdges={}
for v in G.vertices:
AdjEdges[v]=[]
index=0
a=G.vertices[index]
index=index+1
b=G.vertices[index]
index=index+1
Edges.append((a,b))
AdjEdges[a].append((a,b))
Edges.append((b,a))
AdjEdges[b].append((b,a))
m=2
while index < n:
e=Edges[whrandom.randint(0,m-1)]
v=G.vertices[index]
index=index+1
while e[1]!=v:
x=(v,e[0])
Edges.append(x)
m=m+1
AdjEdges[v].append(x)
y=(e[0],v)
Edges.append(y)
m=m+1
AdjEdges[e[0]].insert(AdjEdges[e[0]].index(e)+1,y)
index2=AdjEdges[e[1]].index((e[1],e[0]))
if index2==0:
e=AdjEdges[e[1]][-1]
else:
e=AdjEdges[e[1]][index2-1]
if direction==0: # undirected
m=m-1
while m>0:
del Edges[m]
m=m-2
return Edges
#----------------------------------------------------------------------
class completeGraphCreator(Creator):
def Name(self):
return "create complete graph"
def Create(self, theGraphEditor):
theGraphEditor.config(cursor="watch")
dial = Dialog(theGraphEditor, 0, 0, "create complete graph")
if dial.result is None:
theGraphEditor.config(cursor="")
return
n=dial.result[0]
direction=dial.result[2]
layout=dial.result[3]
G=Graph()
G.directed=direction
for v in range(0,n):
G.AddVertex()
Edges=CompleteEdges(G,n,direction)
for e in Edges:
G.AddEdge(e[0],e[1])
if layout==0:
if RandomCoords(G):
DrawNewGraph(theGraphEditor,G,direction)
else:
if CircularCoords(G):
DrawNewGraph(theGraphEditor,G,direction)
theGraphEditor.config(cursor="")
#----------------------------------------------------------------------
class randomGraphCreator(Creator):
def Name(self):
return "create random graph"
def Create(self, theGraphEditor):
theGraphEditor.config(cursor="watch")
dial = Dialog(theGraphEditor, 0, 1, "create random graph")
if dial.result is None:
theGraphEditor.config(cursor="")
return
n=dial.result[0]
m=dial.result[1]
direction=dial.result[2]
layout=dial.result[3]
G=Graph()
G.directed=direction
for v in range(0,n):
G.AddVertex()
Edges=CompleteEdges(G,n,direction)
for i in range(0,m):
pos=whrandom.randint(0,len(Edges)-1)
G.AddEdge(Edges[pos][0],Edges[pos][1])
del Edges[pos]
if layout==0:
if RandomCoords(G):
DrawNewGraph(theGraphEditor,G,direction)
else:
if CircularCoords(G):
DrawNewGraph(theGraphEditor,G,direction)
theGraphEditor.config(cursor="")
#----------------------------------------------------------------------
class maximalPlanarGraphCreator(Creator):
def Name(self):
return "create maximal planar graph"
def Create(self, theGraphEditor):
theGraphEditor.config(cursor="watch")
dial = Dialog(theGraphEditor, 1, 0, "create maximal planar graph")
if dial.result is None:
theGraphEditor.config(cursor="")
return
n=dial.result[0]
if n<=1: return
direction=dial.result[2]
layout=dial.result[3]
G=Graph()
G.directed=direction
for v in range(0,n):
G.AddVertex()
Edges=MaximalPlanarEdges(G,n,direction)
for e in Edges:
G.AddEdge(e[0],e[1])
if layout==0:
if RandomCoords(G):
DrawNewGraph(theGraphEditor,G,direction)
elif layout==1:
if CircularCoords(G):
DrawNewGraph(theGraphEditor,G,direction)
elif layout==2:
if FPP_PlanarCoords(G):
DrawNewGraph(theGraphEditor,G,direction)
else:
if Schnyder_PlanarCoords(G):
DrawNewGraph(theGraphEditor,G,direction)
theGraphEditor.config(cursor="")
#----------------------------------------------------------------------
from math import log10
class randomPlanarGraphCreator(Creator):
def Name(self):
return "create random planar graph"
def Create(self, theGraphEditor):
theGraphEditor.config(cursor="watch")
dial = Dialog(theGraphEditor, 1, 1, "create random planar graph")
if dial.result is None:
theGraphEditor.config(cursor="")
return
n=dial.result[0]
if n<=1: return
m=dial.result[1]
direction=dial.result[2]
layout=dial.result[3]
G=Graph()
G.directed=direction
for v in range(0,n):
G.AddVertex()
Edges=MaximalPlanarEdges(G,n,direction)
for i in range(0,m):
pos=whrandom.randint(0,len(Edges)-1)
G.AddEdge(Edges[pos][0],Edges[pos][1])
del Edges[pos]
if layout==0:
if RandomCoords(G):
DrawNewGraph(theGraphEditor,G,direction)
elif layout==1:
if CircularCoords(G):
DrawNewGraph(theGraphEditor,G,direction)
elif layout==2:
if FPP_PlanarCoords(G):
DrawNewGraph(theGraphEditor,G,direction)
else:
if Schnyder_PlanarCoords(G):
DrawNewGraph(theGraphEditor,G,direction)
theGraphEditor.config(cursor="")
#----------------------------------------------------------------------
class TreeDialog(tkSimpleDialog.Dialog):
def __init__(self, master, visible, Text):
self.visible=visible
tkSimpleDialog.Dialog.__init__(self, master, Text)
def body(self, master):
self.resizable(0,0)
self.degree=StringVar()
self.degree.set("2")
label = Label(master, text="degree :", anchor=W)
label.grid(row=0, column=0, padx=0, pady=2, sticky="w")
entry=Entry(master, width=6, exportselection=FALSE,
textvariable=self.degree)
entry.selection_range(0,1)
entry.focus_set()
entry.grid(row=0,column=1, padx=2, pady=2, sticky="w")
self.height=StringVar()
self.height.set("0")
label = Label(master, text="height :", anchor=W)
label.grid(row=1, column=0, padx=0, pady=2, sticky="w")
entry=Entry(master, width=6, exportselection=FALSE,
textvariable=self.height)
entry.selection_range(0,1)
entry.focus_set()
entry.grid(row=1,column=1, padx=2, pady=2, sticky="w")
self.number_of_nodes=StringVar()
self.number_of_nodes.set("1")
if self.visible:
label = Label(master, text="#nodes :", anchor=W)
label.grid(row=2, column=0, padx=0, pady=2, sticky="w")
entry=Entry(master, width=6, exportselection=FALSE,
textvariable=self.number_of_nodes)
entry.selection_range(0,1)
entry.focus_set()
entry.grid(row=2,column=1, padx=2, pady=2, sticky="w")
self.direction=IntVar()
self.direction.set(0)
radio=Radiobutton(master, text="Undirected", variable=self.direction,
value=0)
radio.grid(row=0, column=2, padx=2, pady=2, sticky="w")
radio=Radiobutton(master, text="Directed", variable=self.direction,
value=1)
radio.grid(row=1, column=2, padx=2, pady=2, sticky="w")
label = Label(master, text=" ")
label.grid(row=0, column=3, padx=5, pady=2)
self.layout=IntVar()
self.layout.set(0)
radio=Radiobutton(master, text="Randomize Layout",
variable=self.layout, value=0,
width=17, indicatoron=0, selectcolor="white")
radio.grid(row=0, column=4, padx=3, pady=2, sticky="w")
radio=Radiobutton(master, text="Circular Layout",
variable=self.layout, value=1,
width=17, indicatoron=0, selectcolor="white")
radio.grid(row=1, column=4, padx=3, pady=2, sticky="w")
radio=Radiobutton(master, text="Tree Layout",
variable=self.layout, value=2,
width=17, indicatoron=0, selectcolor="white")
radio.grid(row=2, column=4, padx=3, pady=2, sticky="w")
radio=Radiobutton(master, text="BFS-Tree Layout",
variable=self.layout, value=3,
width=17, indicatoron=0, selectcolor="white")
radio.grid(row=3, column=4, padx=3, pady=2, sticky="w")
def validate(self):
try:
d=string.atoi(self.degree.get())
if d<1 or d>20:
raise degreeError
except:
showwarning("Please try again !",
"min. degree = 1\n"
"max. degree = 20")
return 0
try:
h=string.atoi(self.height.get())
if h<0 or h>50:
raise heightError
except:
showwarning("Please try again !",
"min. height = 0\n"
"max. height = 50")
return 0
try:
n=string.atoi(self.number_of_nodes.get())
except:
showwarning("Invalid value !",
"Please enter an integer\n"
"value for #nodes !")
return 0
if self.visible:
if n>1000:
showwarning("Please try again !",
"max. #nodes = 1000")
return 0
min_nodes=h+1
if d==1:
max_nodes=h+1
else:
max_nodes=(float(d)**(h+1)-1)/float(d-1)
if min_nodes>max_nodes:
max_nodes=min_nodes
if max_nodes>1000:
max_nodes=1000
if nmax_nodes:
showwarning("Please try again !",
"min. #nodes = %i\n"
"max. #nodes = %i" %(min_nodes,max_nodes))
return 0
else:
if d==1:
max_height=100
else:
max_height=int(log10(1000)/log10(d))
if h>max_height:
showwarning("Please try again !",
"max. height = %i" %max_height)
return 0
self.result=[]
self.result.append(d)
self.result.append(h)
self.result.append(n)
self.result.append(self.direction.get())
self.result.append(self.layout.get())
return self.result
#----------------------------------------------------------------------
class completeTreeCreator(Creator):
def Name(self):
return "create complete tree"
def Create(self, theGraphEditor):
theGraphEditor.config(cursor="watch")
dial = TreeDialog(theGraphEditor, 0, "create complete tree")
if dial.result is None:
theGraphEditor.config(cursor="")
return
degree=dial.result[0]
height=dial.result[1]
direction=dial.result[3]
layout=dial.result[4]
G=Graph()
G.directed=direction
nodes={}
nodes[0]=[]
G.AddVertex()
nodes[0].append(G.vertices[0])
for h in range(0,height):
nodes[h+1]=[]
for v in nodes[h]:
for d in range(0,degree):
new_v=G.AddVertex()
if direction==0:
G.AddEdge(v,new_v)
else:
if whrandom.randint(0,1):
G.AddEdge(v,new_v)
else:
G.AddEdge(new_v,v)
nodes[h+1].append(new_v)
if layout==0:
if RandomCoords(G):
DrawNewGraph(theGraphEditor,G,direction)
elif layout==1:
if CircularCoords(G):
DrawNewGraph(theGraphEditor,G,direction)
elif layout==2:
if TreeCoords(G,G.vertices[0],"vertical"):
DrawNewGraph(theGraphEditor,G,direction)
else:
if BFSTreeCoords(G,G.vertices[0],"forward"):
DrawNewGraph(theGraphEditor,G,direction)
theGraphEditor.config(cursor="")
#----------------------------------------------------------------------
from math import ceil
class randomTreeCreator(Creator):
def Name(self):
return "create random tree"
def Create(self, theGraphEditor):
theGraphEditor.config(cursor="watch")
dial = TreeDialog(theGraphEditor, 1, "create random tree")
if dial.result is None:
theGraphEditor.config(cursor="")
return
degree=dial.result[0]
height=dial.result[1]
n=dial.result[2]
direction=dial.result[3]
layout=dial.result[4]
G=Graph()
G.directed=direction
nodes={}
nodes[0]=[]
new_v=G.AddVertex()
nodes[0].append(new_v)
children_nr={}
children_nr[new_v]=0
for h in range(0,height):
nodes[h+1]=[]
if degree==1:
min_nodes=1
max_nodes=1
else:
min_nodes=max(1,ceil(float(n-G.Order())/
float((float(degree)**(height-h)-1)/
(degree-1))))
max_nodes=min(n-G.Order()-height+h+1,len(nodes[h])*degree)
nodes_nr=whrandom.randint(min_nodes,max_nodes)
for i in range(0,nodes_nr):
pos=whrandom.randint(0,len(nodes[h])-1)
v=nodes[h][pos]
children_nr[v]=children_nr[v]+1
if children_nr[v]==degree:
del nodes[h][pos]
new_v=G.AddVertex()
children_nr[new_v]=0
if direction==0:
G.AddEdge(v,new_v)
else:
if whrandom.randint(0,1):
G.AddEdge(v,new_v)
else:
G.AddEdge(new_v,v)
nodes[h+1].append(new_v)
if layout==0:
if RandomCoords(G):
DrawNewGraph(theGraphEditor,G,direction)
elif layout==1:
if CircularCoords(G):
DrawNewGraph(theGraphEditor,G,direction)
elif layout==2:
if TreeCoords(G,G.vertices[0],"vertical"):
DrawNewGraph(theGraphEditor,G,direction)
else:
if BFSTreeCoords(G,G.vertices[0],"forward"):
DrawNewGraph(theGraphEditor,G,direction)
theGraphEditor.config(cursor="")
#----------------------------------------------------------------------
from math import ceil
class rectangularGridGraph(Creator):
def Name(self):
return "Create rectangular grid graph"
def Create(self, theGraphEditor):
theGraphEditor.config(cursor="watch")
print " FIXME XXX Dialog for #x, #y, delta_x, delta_y is missing"
## dial = TreeDialog(theGraphEditor, 1, "create random tree")
## if dial.result is None:
## theGraphEditor.config(cursor="")
## return
G=Graph()
G.directed=0
G.xCoord={}
G.yCoord={}
maxI = 10
maxJ = 8
nodes = {}
count = 1
for i in xrange(maxI):
for j in xrange(maxJ):
v = G.AddVertex()
nodes[(i,j)] = v
G.xCoord[v] = j * 40 + 40
G.yCoord[v] = i * 40 + 40
count += 1
for i in xrange(maxI-1):
for j in xrange(maxJ-1):
G.AddEdge(nodes[(i,j)], nodes[(i+1,j)])
G.AddEdge(nodes[(i,j)], nodes[(i,j+1)])
G.AddEdge(nodes[(i,maxJ-1)], nodes[(i+1,maxJ-1)])
for j in xrange(maxJ-1):
G.AddEdge(nodes[(maxI-1,j)], nodes[(maxI-1,j+1)])
DrawNewGraph(theGraphEditor,G,G.directed)
theGraphEditor.config(cursor="")
#----------------------------------------------------------------------
""" Here instantiate all the creators you want to make available to
a client. """
creator = [completeGraphCreator(), randomGraphCreator(),
maximalPlanarGraphCreator(), randomPlanarGraphCreator(),
completeTreeCreator(), randomTreeCreator(), rectangularGridGraph()]
gree :", anchor=W)
label.grid(row=0, column=0, padx=0, pady=2, sticky="w")
entry=Entry(master, width=6, exportselection=FALSE,
textvariablGato/GraphDisplay.py 0100644 0017676 0017534 00000117407 10014153353 0015766 0 ustar 00schliep catbox 0000305 0050066 ################################################################################
#
# This file is part of Gato (Graph Animation Toolbox)
#
# file: GraphDisplay.py
# author: Alexander Schliep (schliep@molgen.mpg.de)
#
# Copyright (C) 1998-2003, Alexander Schliep, Winfried Hochstaettler and
# ZAIK/ZPR, Universitaet zu Koeln
#
# Contact: schliep@molgen.mpg.de, wh@zpr.uni-koeln.de
#
# Information: http://gato.sf.net
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Library General Public
# License as published by the Free Software Foundation; either
# version 2 of the License, or (at your option) any later version.
#
# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Library General Public License for more details.
#
# You should have received a copy of the GNU Library General Public
# License along with this library; if not, write to the Free
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#
#
#
# This file is version $Revision: 1.35 $
# from $Date: 2004/02/11 18:36:01 $
# last change by $Author: schliep $.
#
################################################################################
from Tkinter import * # Frame, Canvas, Toplevel, StringVar and lots of handy constants
import tkFont
from Graph import Graph
from math import sqrt, pi, sin, cos
from GatoGlobals import *
from GatoUtil import orthogonal
from DataStructures import Point2D, VertexLabeling, EdgeLabeling
import os
import colorsys
import logging
log = logging.getLogger("GraphDisplay.py")
class ZoomVar(StringVar):
""" *Internal* helper class to have TK update variable correspoding
to pop-up state """
def __init__(self, graphDisplay,initialValue):
self.graphDisplay = graphDisplay
StringVar.__init__(self)
StringVar.set(self,initialValue)
def set(self, value):
try:
self.graphDisplay.Zoom(value)
except:
None
return StringVar.set(self,value)
class GraphDisplay:
""" Provide functionality to display a graph. Not for direct consumption.
Use
- GraphDisplayToplevel
- GraphDisplayFrame
GraphDisplay also provides UI-Interface independent edit operations
and basic animation methods """
def __init__(self):
self.hasGraph = 0
self.drawVertex = VertexLabeling()
self.drawEdges = EdgeLabeling()
self.drawLabel = VertexLabeling()
self.vertexAnnotation = VertexLabeling()
self.edgeAnnotation = EdgeLabeling()
self.vertex = {} # XXX Dynamic array which memorizes vertex for each draw vertex
self.edge = {} # XXX ditto for draw edge
self.label = {} # XXX ditto for label
self.zoomFactor = 100.0 # percent
self.CreateWidgets()
self.SetTitle("Gato - Graph")
self.update()
self.graphInformer = None
self.clickhandler = None
self.lastAnimation = []
self.highlightedPath = {}
self.ReadConfiguration()
def ReadConfiguration(self):
self.gVertexRadius = 14 # XXX
self.gFontFamily = "Helvetica"
self.gFontSize = 10
self.gFontStyle = tkFont.BOLD
self.gEdgeWidth = 4
self.gVertexFrameWidth = 0
self.cVertexDefault = "#000099" # 'red'
self.cVertexBlink = "black"
self.cEdgeDefault = "#EEEEEE"
self.cLabelDefault = "white"
self.cLabelDefaultInverted = "black"
self.cLabelBlink = "green"
# Used by ramazan's scaling code
self.zVertexRadius = self.gVertexRadius
self.zArrowShape = (16, 20, 6)
self.zFontSize = 10
def font(self, size):
return tkFont.Font(self, (self.gFontFamily, size, self.gFontStyle))
def GetCanvasCenter(self):
""" *Internal* Return the center of the canvas in pixel """
# XXX How to this for non-pixel
return (gPaperWidth/2, gPaperHeight/2)
def Zoom(self,percent):
""" *Internal* Perform a zoom to specified level """
zoomFactor = {' 50 %':50.0,
' 75 %':75.0,
'100 %':100.0,
'125 %':125.0,
'150 %':150.0,
'':100.0}
self.newXview = self.canvas.xview()
self.newYview = self.canvas.yview()
try:
if (self.newXview != self.oldXview or
self.newYview != self.oldYview or
self.zoomIn == 0):
self.Xview = self.newXview[0]
self.Yview = self.newYview[0]
except:
self.Xview = self.newXview[0]
self.Yview = self.newYview[0]
if zoomFactor[percent] < self.zoomFactor:
self.zoomIn = 1
else:
self.zoomIn = 0
factor = zoomFactor[percent] / self.zoomFactor
self.zoomFactor = zoomFactor[percent]
self.zVertexRadius = (self.gVertexRadius*self.zoomFactor) / 100.0
self.zArrowShape = ((16*self.zoomFactor) / 100.0,
(20*self.zoomFactor) / 100.0,
(6*self.zoomFactor) / 100.0)
self.zFontSize = max(7,int((self.gFontSize*self.zoomFactor) / 100.0))
for v in self.G.vertices:
dv = self.drawVertex[v]
oldVertexFrameWidth = self.canvas.itemcget(dv, "width")
newVertexFrameWidth = float(oldVertexFrameWidth) * factor
self.canvas.itemconfig(dv, width=newVertexFrameWidth)
dl = self.drawLabel[v]
self.canvas.itemconfig(dl, font=self.font(self.zFontSize))
for e in self.G.Edges():
de = self.drawEdges[e]
oldEdgeWidth = self.canvas.itemcget(de, "width")
newEdgeWidth = float(oldEdgeWidth) * factor
self.canvas.itemconfig(de, width=newEdgeWidth,
arrowshape=self.zArrowShape)
self.canvas.scale("all", 0, 0, factor, factor)
newWidth = (self.zoomFactor / 100.0) * float(gPaperWidth)
newHeight = (self.zoomFactor/ 100.0) * float(gPaperHeight)
self.canvas.config(width=newWidth,height=newHeight,
scrollregion=(0,0,newWidth,newHeight))
self.canvas.xview("moveto",self.Xview)
self.canvas.yview("moveto",self.Yview)
self.oldXview = self.canvas.xview()
self.oldYview = self.canvas.yview()
def CanvasToEmbedding(self,x,y):
""" *Internal* Convert canvas coordinates to embedding """
x = x * 100.0 / self.zoomFactor
y = y * 100.0 / self.zoomFactor
return x,y
def EmbeddingToCanvas(self,x,y):
""" *Internal* Convert Embedding coordinates to Canvas """
x = x * self.zoomFactor / 100.0
y = y * self.zoomFactor / 100.0
return x,y
def CreateWidgets(self):
""" *Internal* Create UI-Elements (except Frame/Toplevel) """
# Frame at bottom with zoom-popup and label
self.infoframe = Frame(self, relief=FLAT)
self.infoframe.pack(side=BOTTOM, fill=X)
self.zoomValue = ZoomVar(self,'100 %')
# XXX self.zoomValue.set('100 %')
self.zoomMenu = OptionMenu(self.infoframe, self.zoomValue,
' 50 %',' 75 %', '100 %','125 %','150 %')
self.zoomMenu.config(height=1)
self.zoomMenu.config(width=5)
if os.name == 'mac':
self.zoomMenu.config(font="Geneva 10")
self.zoomMenu["menu"].config(font="Geneva 10")
self.zoomMenu.grid(row=0,column=0,sticky="nwse")
self.infoframe.columnconfigure(0,weight=0)
# To make things more windows like, we put the info-label
# in a separate frame
borderFrame = Frame(self.infoframe, relief=SUNKEN, bd=1)
self.info = Label(borderFrame, text="No information available", anchor=W)
self.info.config(width=50)
if os.name == 'mac':
self.info.config(font="Geneva 10")
self.info.pack(side=LEFT, expand=1, fill=X)
borderFrame.grid(row=0,column=1,sticky="nwse",padx=4,pady=3)
self.infoframe.columnconfigure(1,weight=1)
# Scrolling Canvas
# To make things more windows like, we put the canvas
# in a separate frame
borderFrame = Frame(self, relief=SUNKEN, bd=2)
self.canvas = Canvas(borderFrame, width=gPaperWidth, height=gPaperHeight,
background="white",
scrollregion=(0, 0, gPaperWidth, gPaperHeight))
# Vertical scroll bar in a frame and with corner
vbarFrame = Frame(borderFrame,borderwidth=0)
vbarFrame.pack(fill=Y, side=RIGHT)
self.canvas.vbar = Scrollbar(borderFrame, orient=VERTICAL)
self.canvas['yscrollcommand'] = self.canvas.vbar.set
self.canvas.vbar['command'] = self.canvas.yview
self.canvas.vbar.pack(in_=vbarFrame, expand=1, side=TOP, fill=Y)
sbwidth = self.canvas.vbar.winfo_reqwidth()
corner = Frame(vbarFrame, width=sbwidth, height=sbwidth)
corner.propagate(0)
corner.pack(side=BOTTOM)
# Horizontal scroll bar
self.canvas.hbar = Scrollbar(borderFrame, orient=HORIZONTAL)
self.canvas['xscrollcommand'] = self.canvas.hbar.set
self.canvas.hbar['command'] = self.canvas.xview
self.canvas.hbar.pack(side=BOTTOM, fill=X)
self.canvas.pack(anchor=W, side=TOP)
borderFrame.pack(anchor=W, side=TOP, expand=1, fill=BOTH)
try:
self.geometry("500x483")
except:
self.master.geometry("500x483")
def ShowGraph(self, G, graphName):
""" Display graph G name graphName. Currently we assume that for
the embedding (x,y) of every vertex < x < 1000 and 0 < y < 1000
holds.
NOTE: We need both a proper embedding and a labelling
XXX: Fix (Randomize embedding, identity labeling if none given """
self.G = G
self.embedding = G.embedding # Assuming 0 < x < 1000 and 0 < y < 1000
self.Labeling = G.labeling
self.directed = G.QDirected()
if self.hasGraph == 1:
self.DeleteDrawItems()
self.CreateDrawItems()
self.hasGraph = 1
self.SetTitle("Gato - " + graphName)
self.update()
self.DefaultInfo()
def RegisterGraphInformer(self, Informer):
""" A graph informer is an object which supplies information
about the graph, its vertices and its edges. It needs methods
- DefaultInfo()
- VertexInfo(v)
- EdgeInfo(tail,head)
If none is registered, information will be produced by
GraphDisplay. Infos are displayed in info field at the bottom
of the graph window."""
self.graphInformer = Informer
def CreateDrawItems(self):
""" *Internal* Create items on the canvas """
for v in self.G.vertices:
for w in self.G.OutNeighbors(v):
self.drawEdges[(v,w)] = self.CreateDrawEdge(v,w)
for v in self.G.vertices:
x = self.embedding[v].x * self.zoomFactor / 100.0
y = self.embedding[v].y * self.zoomFactor / 100.0
self.drawVertex[v] = self.CreateDrawVertex(v,x,y)
for v in self.G.vertices:
self.drawLabel[v] = self.CreateDrawLabel(v)
def DeleteDrawItems(self):
""" *Internal* Delete all items on the canvas and clear up
our references to it"""
self.DeleteDrawEdges()
self.DeleteDrawVertices()
self.DeleteDrawLabels()
self.DeleteVertexAnnotations()
self.DeleteEdgeAnnotations()
self.canvas.delete("all") # Remove whatever is left
self.drawVertex = VertexLabeling()
self.drawEdges = EdgeLabeling()
self.drawLabel = VertexLabeling()
self.vertexAnnotation = VertexLabeling()
self.edgeAnnotation = EdgeLabeling()
def DeleteDrawEdges(self):
""" *Internal* Delete draw edges on the canvas """
self.edge = {} # XXX
self.canvas.delete("edges")
def DeleteDrawVertices(self):
""" *Internal* Delete draw vertices on the canvas """
self.vertex = {} # XXX
self.canvas.delete("vertices")
def DeleteDrawLabels(self):
""" *Internal* Delete draw labels on the canvas """
self.label = {} # XXX
self.canvas.delete("labels")
def DeleteVertexAnnotations(self):
""" *Internal* Delete all vertex annotations on the canvas """
self.canvas.delete("vertexAnno")
pass
def DeleteEdgeAnnotations(self):
""" *Internal* Delete all edge annotations on the canvas """
self.canvas.delete("edgeAnno")
pass
def CreateDrawVertex(self,v,x=None,y=None):
""" *Internal* Create a draw vertex for v on the canvas. Position is
determined by the embedding unless explictely passed as x,y in
canvas coordinates """
if x == None and y == None:
x,y = self.EmbeddingToCanvas(self.embedding[v].x, self.embedding[v].y)
d = self.zVertexRadius
w = (self.gVertexFrameWidth*self.zoomFactor) / 100.0
dv = self.canvas.create_oval(x-d, y-d, x+d, y+d,
fill=self.cVertexDefault,
tag="vertices",
width=w)
self.canvas.tag_bind(dv, "", self.DefaultInfo)
self.canvas.tag_bind(dv, "", self.VertexInfo)
self.vertex[dv] = v # XXX
return dv
def CreateDrawLabel(self,v):
""" *Internal* Create a draw label for v on the canvas. Position is
determined by the embedding specified. Text is specified by the
labeling:
Call only after CreateDrawVertex() """
pos = self.VertexPosition(v)
# To make label more readable on darker vertices we change colors
# depending on brightness
#
# XXX Note: we assume that the defaults are reasonable
dl = self.canvas.create_text(pos.x, pos.y,
anchor="center",
justify="center",
font=self.font(self.zFontSize),
text=self.Labeling[v],
fill=self.cLabelDefault,
tag="labels")
self.canvas.tag_bind(dl, "", self.VertexInfo)
self.label[dl] = v # XXX
return dl
# Label to the bottom, to the right
#d = self.zVertexRadius
#return self.canvas.create_text(x+d+1, y+d+1, anchor="w", justify="left", font="Arial %d" %self.zFontSize,text=v)
def CreateUndirectedLoopDrawEdge(self, v, w, orientation=None):
""" *Internal* Create an undirected loop draw edge. v is a Point2D """
loopRadius = 2 * self.zVertexRadius
xMiddle = v.x
yMiddle = v.y-((25*self.zoomFactor)/100.0)
Coords = []
for degree in range(0,400,40):
Coords.append(loopRadius*cos(degree*(pi/180))+xMiddle)
Coords.append(loopRadius*sin(degree*(pi/180))+yMiddle)
return self.canvas.create_line(Coords,
fill=self.cEdgeDefault,
width=w,
smooth=TRUE,
splinesteps=24,
tag="edges")
def CreateDirectedLoopDrawEdge(self,v,w, orientation=None):
""" *Internal* Create an directed loop draw edge. v is a Point2D """
loopRadius = 2 * self.zVertexRadius
xMiddle = v.x
yMiddle = v.y-((25*self.zoomFactor)/100.0)
Coords = []
for degree in range(95,440,25):
if degree != 395:
Coords.append(loopRadius*cos(degree*(pi/180))+xMiddle)
Coords.append(loopRadius*sin(degree*(pi/180))+yMiddle)
return self.canvas.create_line(Coords,
arrow="last",
arrowshape=self.zArrowShape,
fill=self.cEdgeDefault,
width=w,
smooth=TRUE,
splinesteps=24,
tag="edges")
def CreateUndirectedDrawEdge(self,t,h,w):
""" *Internal* Create an undirected draw edge. t, h are Point2Ds """
return self.canvas.create_line(t.x,t.y,h.x,h.y,
fill=self.cEdgeDefault,
width=w,
tag="edges")
def CreateDirectedDrawEdge(self,t,h,curved,w):
""" *Internal* Create an directed draw edge. t, h are Point2Ds """
l = sqrt((h.x - t.x)**2 + (h.y - t.y)**2)
if l < 0.001:
l = 0.001
c = (l - self.zVertexRadius)/l - 0.001 # Dont let them quite touch
# (tmpX,tmpY) is a point on a straight line between t and h
# not quite touching the vertex disc
tmpX = t.x + c * (h.x - t.x)
tmpY = t.y + c * (h.y - t.y)
if curved == 0:
return self.canvas.create_line(t.x,t.y,tmpX,tmpY,
fill=self.cEdgeDefault,
arrow="last",
arrowshape=self.zArrowShape,
width=w,
tag="edges")
else:
# (mX,mY) to difference vector h - t
(mX,mY) = orthogonal((h.x - t.x, h.y - t.y))
c = 1.5 * self.zVertexRadius + l / 25
# Add c * (mX,mY) at midpoint between h and t
mX = t.x + .5 * (h.x - t.x) + c * mX
mY = t.y + .5 * (h.y - t.y) + c * mY
return self.canvas.create_line(t.x,t.y,mX,mY,tmpX,tmpY,
fill=self.cEdgeDefault,
arrow="last",
arrowshape=self.zArrowShape,
width=w,
smooth=TRUE,
tag="edges")
def CreateDrawEdge(self,tail,head):
""" *Internal* Create a draw edge for (tail,head) on the canvas. Position is
determined by the position of the vertices (or the embedding if the draw
vertices do not exist yet)."""
t = self.VertexPosition(tail)
h = self.VertexPosition(head)
if self.G.edgeWidth == None:
w = (self.gEdgeWidth * self.zoomFactor) / 100.0
else:
w = (self.G.edgeWidth[(tail,head)] * self.zoomFactor) / 100.0
if self.directed == 1:
if tail == head:
de = self.CreateDirectedLoopDrawEdge(t,w)
else:
if tail in self.G.adjLists[head]:
# Remove old straight de for other direction ...
try:
oldColor = self.canvas.itemconfig(self.drawEdges[(head,tail)],
"fill")[4] # Should call GetEdgeColor
self.canvas.delete(self.drawEdges[(head,tail)])
# ... and create a new curved one
if self.G.edgeWidth == None:
wOld = (self.gEdgeWidth * self.zoomFactor) / 100.0
else:
wOld = (self.G.edgeWidth[(head,tail)] * self.zoomFactor) / 100.0
de = self.CreateDirectedDrawEdge(h,t,1,wOld)
self.canvas.itemconfig(de, fill=oldColor) # Should call SetEdgeColor
self.drawEdges[(head,tail)] = de
self.edge[de] = (head,tail)
self.canvas.tag_bind(de, "", self.DefaultInfo)
self.canvas.tag_bind(de, "", self.EdgeInfo)
try:
self.canvas.lower(de,"vertices")
except TclError:
None # can get here when opening graph
except KeyError:
oldColor = self.cEdgeDefault # When opening a graph we can get here
# Finally create the one we wanted to ...
de = self.CreateDirectedDrawEdge(t,h,1,w)
else:
de = self.CreateDirectedDrawEdge(t,h,0,w)
else:
if tail == head:
de = self.CreateUndirectedLoopDrawEdge(t,w)
else:
de = self.CreateUndirectedDrawEdge(t,h,w)
self.edge[de] = (tail,head) # XXX
self.canvas.tag_bind(de, "", self.DefaultInfo)
self.canvas.tag_bind(de, "", self.EdgeInfo)
return de
def CreateVertexAnnotation(self,v,annotation,color):
""" *Internal* Create a vertex annotation for v on the canvas. Position is
determined by the position of the corresponding draw vertex
on the canvas. """
pos = self.VertexPosition(v)
# Label to the bottom, to the right
da = self.canvas.create_text(pos.x + self.zVertexRadius+1,
pos.y + self.zVertexRadius+1,
anchor="w",
justify="left",
font=self.font(self.zFontSize),
text=annotation,
tag="vertexAnno",
fill=color)
return da
def CreateEdgeAnnotation(self,tail,head,annotation,color):
""" *Internal* Create an edge annotation for (tail,head) on the canvas.
Position is determined by the embedding specified. """
t = self.VertexPosition(tail)
h = self.VertexPosition(head)
(mX,mY) = orthogonal((h.x - t.x, h.y - t.y))
c = self.zVertexRadius
x = t.x + .5 * (h.x - t.x) + c * mX
y = t.y + .5 * (h.y - t.y) + c * mY
# Label to the bottom, to the right
da = self.canvas.create_text(x, y,
anchor="center",
justify="center",
font=self.font(self.zFontSize),
text=annotation,
tag="edgeAnno",
fill=color)
return da
############################################################################
#
# Animation history
#
def animate(self,elem_type, elem, canvas_elem):
self.lastAnimation = [elem_type, elem, canvas_elem]
def animateVertex(self, vertex, draw_vertex):
self.animate('vertex', vertex, draw_vertex)
def animateEdge(self, edge, draw_edge):
self.animate('edge', edge, draw_edge)
def highlightLastAnimation(self, dummy):
if self.lastAnimation is not None:
if self.lastAnimation[0] == 'vertex':
self.BlinkVertex(self.lastAnimation[1],'yellow')
else:
self.BlinkEdge(self.lastAnimation[1][0], self.lastAnimation[1][1],'yellow')
############################################################################
#
# Animator commands
#
def SetVertexColor(self, v, color):
""" Change color of v to color. No error checking! """
rgb_color = self.winfo_rgb(color)
# Tk has 16 bits per color
hls_color = colorsys.rgb_to_hls(rgb_color[0] / 65536.0,
rgb_color[1] / 65536.0,
rgb_color[2] / 65536.0)
lightness = hls_color[1]
if lightness < 0.2:
self.canvas.itemconfig( self.drawLabel[v], fill=self.cLabelDefaultInverted)
else:
self.canvas.itemconfig( self.drawLabel[v], fill=self.cLabelDefault)
self.canvas.itemconfig( self.drawVertex[v], fill=color)
self.update()
self.animateVertex(v, self.drawVertex[v])
def GetVertexColor(self,v):
""" Return the color of v """
dv = self.drawVertex[v]
return self.canvas.itemconfig(dv, "fill")[4]
def SetAllVerticesColor(self,color,graph=None):
""" Change the color of all vertices to 'color' at once
You can also pass an induced subgraph """
if graph == None:
self.canvas.itemconfig("vertices", fill=color)
else: # induced subgraph
for v in graph.vertices:
self.canvas.itemconfig(self.drawVertex[v], fill=color)
self.update()
self.lastAnimation = []
def SetAllEdgesColor(self,color,graph=None, leaveColors = None):
""" Change the color of all edges to 'color' at once
You can also pass an induced subgraph """
if graph == None:
if leaveColors == None:
self.canvas.itemconfig("edges", fill=color)
else:
for e in self.G.Edges():
if not self.GetEdgeColor(e[0],e[1]) in leaveColors:
self.SetEdgeColor(e[0],e[1],color)
else: # induced subgraph
for e in graph.Edges():
if leaveColors == None or not (self.GetEdgeColor(e[0],e[1]) in leaveColors):
self.SetEdgeColor(e[0],e[1],color)
self.update()
self.lastAnimation = []
def SetEdgeColor(self, tail, head, color):
""" Change color of (tail,head) to color. No error checking!
Handles undirected graphs. """
if self.directed == 1:
de = self.drawEdges[(tail,head)]
else:
try:
de = self.drawEdges[(tail,head)]
except KeyError:
de = self.drawEdges[(head,tail)]
self.canvas.itemconfig( de, fill=color)
self.update()
self.animateEdge((tail,head), de)
def GetEdgeColor(self, tail, head):
""" Return color of (tail,head). No error checking!
Handles undirected graphs. """
(u,v) = self.G.Edge(tail,head)
de = self.drawEdges[(u,v)]
return self.canvas.itemconfig(de, "fill")[4]
def BlinkVertex(self, v, color=None):
""" Blink vertex v with color. Number of times, speed, default color is
specified in GatoGlobals.py. No error checking! """
if color is None: # No self in default arg
color=self.cVertexBlink
dv = self.drawVertex[v]
oldColor = self.canvas.itemconfig(dv, "fill")[4]
for i in xrange(1,gBlinkRepeat):
self.canvas.after(gBlinkRate)
self.canvas.itemconfig( dv, fill=color)
self.update()
self.canvas.after(gBlinkRate)
self.canvas.itemconfig( dv, fill=oldColor)
self.update()
self.animateVertex(v, self.drawVertex[v])
def BlinkEdge(self, tail, head, color=None):
""" Blink edge (tail,head) with color. Number of times, speed, default
color is specified in GatoGlobals.py. No error checking! Handles
undirected graphs. """
if color is None: # No self in default arg
color=self.cVertexBlink
if self.directed == 1:
de = self.drawEdges[(tail,head)]
else:
try:
de = self.drawEdges[(tail,head)]
except KeyError:
de = self.drawEdges[(head,tail)]
oldColor = self.canvas.itemconfig(de, "fill")[4]
for i in xrange(1,gBlinkRepeat):
self.canvas.after(gBlinkRate)
self.canvas.itemconfig( de, fill=color)
self.update()
self.canvas.after(gBlinkRate)
self.canvas.itemconfig( de, fill=oldColor)
self.update()
self.animateEdge((tail, head), de)
def Blink(self, list, color=None):
""" Blink all edges or vertices in list with color.
Edges are specified as (tail,head).
Number of times, speed, default color is specified in GatoGlobals.py.
No error checking! Handles undirected graphs. """
if color is None: # No self in default arg
color=self.cVertexBlink
oldColor = [None] * len(list)
drawItems = [None] * len(list)
for i in xrange(len(list)):
try:
e = list[i]
l = len(e) # will raise an exception
drawItems[i] = self.drawEdges[e]
oldColor[i] = self.canvas.itemconfig(drawItems[i], "fill")[4]
except: # It is a vertex
v = list[i]
drawItems[i] = self.drawVertex[v]
oldColor[i] = self.canvas.itemconfig(drawItems[i], "fill")[4]
for i in xrange(1,gBlinkRepeat):
self.canvas.after(gBlinkRate)
for j in xrange(len(drawItems)):
self.canvas.itemconfig(drawItems[j], fill=color)
self.update()
self.canvas.after(gBlinkRate)
for j in xrange(len(drawItems)):
self.canvas.itemconfig(drawItems[j], fill=oldColor[j])
self.update()
def SetVertexFrameWidth(self,v,val):
""" Set the width of the black frame of a vertex to val """
dv = self.drawVertex[v]
oldwidth = float(self.canvas.itemcget(dv, "width"))
self.canvas.itemconfig(dv, outline = "white", width=oldwidth * 2)
self.update()
self.canvas.itemconfig(dv, outline = "black", width=(val * self.zoomFactor) / 100.0)
self.update()
self.animateVertex(v, dv)
def SetVertexAnnotation(self,v,annotation,color="black"):
""" Add an annotation to v. Annotations are displayed to the left and
the bottom of v and allow to display more info about a vertex.
No error checking! Does not handle vertex deletions/moves !"""
if v == None: return
if not self.vertexAnnotation.QDefined(v):
self.vertexAnnotation[v] = self.CreateVertexAnnotation(v,annotation,color)
else:
da = self.vertexAnnotation[v]
self.canvas.itemconfig(da,
font=self.font(self.zFontSize),
text=annotation,
fill=color)
self.update()
self.animateVertex(v, self.drawVertex[v])
def SetEdgeAnnotation(self,tail,head,annotation,color="black"):
""" Add an annotation to (tail,head). Annotations are displayed to the left and
the bottom of v and allow to display more info about a vertex.
No error checking! Does not handle edge deletions/moves !"""
if not self.edgeAnnotation.QDefined((tail,head)):
self.edgeAnnotation[(tail,head)] = self.CreateEdgeAnnotation(tail,head,
annotation,
color)
else:
da = self.edgeAnnotation[(tail,head)]
self.canvas.itemconfig(da,
font=self.font(self.zFontSize),
text=annotation,
fill=color)
self.update()
self.animateEdge((tail, head), self.drawEdge[(tail,head)])
def UpdateVertexLabel(self, v, blink=1, color=None):
""" Visualize the changing of v's label. After changing G.labeling[v],
call UpdateVertexLabel to update the label in the graph window,
blinking blink times with color. No error checking! """
if color is None:
color=self.cLabelBlink
dl = self.drawLabel[v]
if blink == 1:
oldColor = self.canvas.itemconfig(dl, "fill")[4]
for i in xrange(1,gBlinkRepeat):
self.canvas.after(gBlinkRate)
self.canvas.itemconfig( dl, fill=color)
self.update()
self.canvas.after(gBlinkRate)
self.canvas.itemconfig( dl, fill=oldColor)
self.update()
self.canvas.itemconfig( dl,
font=self.font(self.zFontSize),
text=self.Labeling[v])
else:
self.canvas.itemconfig( dl,
font=self.font(self.zFontSize),
text=self.Labeling[v])
self.update()
self.animateVertex(v, self.drawVertex[v])
def UpdateInfo(self, neuText):
""" *Internal* Update text in info box """
self.info.config(text=neuText)
self.update()
def DefaultInfo(self,event=None):
""" *Internal* Put default info into info box """
if self.graphInformer == None:
self.UpdateInfo("")
else:
self.UpdateInfo(self.graphInformer.DefaultInfo())
def VertexInfo(self,event):
""" *Internal* Call back routine bound to MouseEnter of vertices and
labels. Produces default info for vertices unless a user supplied
informer has been registered with RegisterGraphInformer() """
widget = event.widget.find_withtag(CURRENT)[0]
tags = self.canvas.gettags(widget)
if "vertices" in tags:
v = self.vertex[widget]
elif "labels" in tags:
v = self.label[widget]
else:
return
if self.graphInformer == None:
infoString = "Vertex %d at position (%d,%d)" % (v,
self.embedding[v].x,
self.embedding[v].y)
else:
infoString = self.graphInformer.VertexInfo(v)
self.UpdateInfo(infoString)
def EdgeInfo(self,event):
""" *Internal* Call back routine bound to MouseEnter of edges.
Produces default info for edges unless a user supplied
informer has been registered with RegisterGraphInformer() """
widget = event.widget.find_withtag(CURRENT)[0]
(tail,head) = self.edge[widget]
if self.graphInformer == None:
infoString = "Edge (%d,%d)" % (tail, head)
else:
infoString = self.graphInformer.EdgeInfo(tail,head)
self.UpdateInfo(infoString)
def FindVertex(self,event):
""" *Internal* Given an event find the correspoding vertex """
if not event.widget.find_withtag(CURRENT):
return None
else:
try:
widget = event.widget.find_withtag(CURRENT)[0]
tags = self.canvas.gettags(widget)
if "vertices" in tags:
v = self.vertex[widget]
elif "labels" in tags:
v = self.label[widget]
else:
v = None
return v
except:
return None
def FindGridVertex(self,event):
""" *Internal* Given an event find the correspoding grid vertex """
x,y = self.WindowToCanvasCoords(event)
if not event.widget.find_overlapping(x,y,x,y):
return None
else:
try:
widget = event.widget.find_overlapping(x,y,x,y)[-1]
tags = self.canvas.gettags(widget)
if "vertices" in tags:
v = self.vertex[widget]
elif "labels" in tags:
v = self.label[widget]
else:
v = None
return v
except:
return None
def FindEdge(self,event):
""" *Internal* Given an event find the correspoding edge """
if not event.widget.find_withtag(CURRENT):
return None
else:
try:
widget = event.widget.find_withtag(CURRENT)[0]
e = self.edge[widget]
return e
except:
return None
def HighlightPath(self, path, color, closed = 0):
""" Draw a wide poly line underneath the path in the graph """
pathID = tuple(path)
coords = ()
for v in path:
coords += (self.embedding[v].x * self.zoomFactor / 100.0,
self.embedding[v].y * self.zoomFactor / 100.0)
if closed:
coords += (self.embedding[path[0]].x * self.zoomFactor / 100.0,
self.embedding[path[0]].y * self.zoomFactor / 100.0)
c = self.canvas.create_line(coords, tag="highlight", fill=color,
width = 16)
self.canvas.lower(c,"edges")
self.highlightedPath[pathID] = c
return pathID
def HidePath(self, pathID):
self.canvas.delete(self.highlightedPath[pathID])
############################################################################
#
# edit commands
#
def AddVertex(self, x, y):
""" *Internal* Add a new vertex at (x,y)
NOTE: Assumes x,y to be in embedding coordinates"""
v = self.G.AddVertex()
self.embedding[v] = Point2D(x,y)
self.Labeling[v] = v
self.drawVertex[v] = self.CreateDrawVertex(v)
self.drawLabel[v] = self.CreateDrawLabel(v)
for i in xrange(0,self.G.NrOfVertexWeights()):
self.G.vertexWeights[i][v] = 0
return v
def AddVertexCanvas(self, x, y):
""" *Internal* Add a new vertex at (x,y)
NOTE: Assumes x,y to be in canvas coordinates"""
v = self.G.AddVertex()
embed_x, embed_y = self.CanvasToEmbedding(x,y)
self.embedding[v] = Point2D(embed_x,embed_y)
self.Labeling[v] = v
self.drawVertex[v] = self.CreateDrawVertex(v,x,y)
self.drawLabel[v] = self.CreateDrawLabel(v)
for i in xrange(0,self.G.NrOfVertexWeights()):
self.G.vertexWeights[i][v] = 0
return v
def MoveVertex(self,v,x,y,doUpdate=None):
""" *Internal* Move vertex v to position (x,y)
NOTE: Assumes x,y to be in canvas coordinates if
doUpdate=None and in embedding coordinates else
"""
if doUpdate == None: # User has moved drawvertex
newX, newY = self.CanvasToEmbedding(x,y)
self.embedding[v] = Point2D(newX, newY)
else:
# Here translation of canvas does not matter, since we
# move vertex relatively anyways
pos = self.VertexPosition(v)
canvas_x,canvas_y = self.EmbeddingToCanvas(x,y)
dx = canvas_x - pos.x
dy = canvas_y - pos.y
dv = self.drawVertex[v]
self.canvas.move(dv, dx, dy)
self.canvas.move(self.drawLabel[v], dx, dy)
self.embedding[v] = Point2D(x,y)
# move incident edges
outVertices = self.G.OutNeighbors(v)[:] # Need a copy here
inVertices = self.G.InNeighbors(v)[:]
euclidian = self.G.QEuclidian()
# Handle outgoing edges
t = self.embedding[v]
for w in outVertices:
de = self.drawEdges[(v,w)]
self.canvas.delete(de)
de = self.CreateDrawEdge(v,w)
self.drawEdges[(v,w)] = de
self.canvas.lower(de,"vertices")
if euclidian:
h = self.embedding[w]
self.G.edgeWeights[0][(v,w)] = sqrt((h.x - t.x)**2 + (h.y - t.y)**2)
# Handle incoming edges
h = self.embedding[v]
for w in inVertices:
de = self.drawEdges[(w,v)]
self.canvas.delete(de)
de = self.CreateDrawEdge(w,v)
self.drawEdges[(w,v)] = de
self.canvas.lower(de,"vertices")
if euclidian:
t = self.embedding[w]
self.G.edgeWeights[0][(w,v)] = sqrt((h.x - t.x)**2 + (h.y - t.y)**2)
def DeleteVertex(self,v):
""" *Internal* Delete vertex v """
del(self.Labeling.label[v]) # XXX
del(self.embedding.label[v]) # XXX
# if v has an annotation delete
if self.vertexAnnotation.QDefined(v):
self.canvas.delete(self.vertexAnnotation[v])
del(self.vertexAnnotation.label[v])
self.canvas.delete(self.drawVertex[v])
del(self.drawVertex.label[v])
self.canvas.delete(self.drawLabel[v])
del(self.drawLabel.label[v])
## # delete incident edges
## outVertices = self.G.OutNeighbors(v)[:] # Need a copy here
## inVertices = self.G.InNeighbors(v)[:]
## for w in outVertices:
## self.DeleteEdge(v,w,0)
## for w in inVertices:
## if w != v: # We have already deleted loops
## self.DeleteEdge(w,v,0)
## #del(self.G.adjLists[v]) # XXX
## # and finally the vertex itself
## self.G.vertices.remove(v) # XXX
self.G.DeleteVertex(v)
def AddEdge(self,tail,head):
""" *Internal* Add Edge. Note: unless graph is Euclidian weight is set
to 0. No error checking !"""
try:
self.G.AddEdge(tail,head)
de = self.CreateDrawEdge(tail,head)
self.drawEdges[(tail, head)] = de
self.canvas.lower(de,"vertices")
if self.G.QEuclidian():
t = self.embedding[tail]
h = self.embedding[head]
self.G.edgeWeights[0][(tail,head)] = sqrt((h.x - t.x)**2 + (h.y - t.y)**2)
else:
self.G.edgeWeights[0][(tail,head)] = 0
for i in xrange(1,self.G.NrOfEdgeWeights()):
self.G.edgeWeights[i][(tail,head)] = 0
except GraphNotSimpleError:
log.error("Inserting edge would result in non-simple graph")
return
def DeleteEdge(self,tail,head,repaint=1):
""" *Internal* Delete edge (tail,head) """
self.canvas.delete(self.drawEdges[(tail,head)])
# if (tail,head) has an annotation delete it
if self.edgeAnnotation.QDefined((tail,head)):
self.canvas.delete(self.edgeAnnotation[(tail,head)])
del(self.edgeAnnotation.label[(tail,head)])
del(self.drawEdges.label[(tail,head)]) # XXX
self.G.DeleteEdge(tail,head)
if repaint and self.directed == 1 and tail in self.G.adjLists[head]:
# i.e. parallel edge
oldColor = self.canvas.itemconfig(self.drawEdges[(head,tail)],
"fill")[4] # Should call GetEdgeColor
self.canvas.delete(self.drawEdges[(head,tail)])
de = self.CreateDrawEdge(head,tail)
self.canvas.itemconfig(de, fill=oldColor) # Should call SetEdgeColor
self.drawEdges[(head,tail)] = de
self.canvas.lower(de,"vertices")
def SwapEdgeOrientation(self,tail,head):
""" *Internal* If graph is directed and we do not have edges in both
directions, change the orientation of the edge (tail,head) """
if self.directed == 0 or self.G.QEdge(head,tail): # Assuming (tail,head) is an edge
return
self.DeleteEdge(tail,head)
self.AddEdge(head,tail)
def VertexPosition(self,v):
""" Return the position of vertex v in canvas coordinates """
try:
coords = self.canvas.coords(self.drawVertex[v])
x = 0.5 * (coords[2] - coords[0]) + coords[0]
y = 0.5 * (coords[3] - coords[1]) + coords[1]
except: # Vertex is not on the canvas yet
x,y = self.EmbeddingToCanvas(self.embedding[v].x,self.embedding[v].y)
return Point2D(x,y)
############################################################################
#
# various stuff
#
def PrintToPSFile(self,fileName):
""" Produce an EPSF of canvas in fileName. Note: Graph gets scaled
and rotated as to maximize size while still fitting on paper """
bb = self.canvas.bbox("all") # Bounding box of all elements on canvas
# Give 10 pixels room to breathe
x = max(bb[0] - 10,0)
y = max(bb[1] - 10,0)
width=bb[2] - bb[0] + 10
height=bb[3] - bb[1] + 10
printablePageHeight=280 #m
printablePageWidth =190 #m
printableRatio=printablePageHeight/printablePageWidth
bbRatio = height/width
if bbRatio > printableRatio: # Height gives limiting dimension
self.canvas.postscript(file=fileName, pageheight="%dm" % printablePageHeight,
x=x,y=y,height=height,width=width)
else:
self.canvas.postscript(file=fileName, pagewidth="%dm" % printablePageWidth,
x=x,y=y,height=height,width=width)
def About(self):
""" Return a HTML-page giving information about the graph """
if self.hasGraph == 1:
return self.G.About()
else:
return " No information available
"
############################################################################
#
# Clickhandler commands
#
def RegisterClickhandler(self, handler):
""" A clickhandler is a function being called when the user
clicks on a vertex or an edge (actually releases mouse
button 1 over a vertex or an edge).
The clickhandler takes a string 'vertex' or 'edge' as the
first and the vertex/edge clicked on as the second argument """
self.clickhandler = handler
self.canvas.bind("", self.MouseUp)
def UnregisterClickhandler(self):
""" Unregister the handler """
self.clickhandler = None
self.canvas.unbind("")
def MouseUp(self, event):
""" Callback method for . Finds the vertex/edge
clicked and calls the registered clickhandler """
if self.clickhandler != None:
v = self.FindVertex(event)
if v != None:
self.clickhandler('vertex',v)
else:
e = self.FindEdge(event)
if e != None:
self.clickhandler('edge',e)
class GraphDisplayFrame(GraphDisplay, Frame):
""" Provides graph display in a frame """
def __init__(self, master=None):
Frame.__init__(self, master)
self.pack()
self.pack(expand=1,fill=BOTH) # Makes whole window resizeable
GraphDisplay.__init__(self)
def SetTitle(self,title):
log.info("change window title to %s" % title)
class GraphDisplayToplevel(GraphDisplay, Toplevel):
""" Provides graph display in a top-level window """
def __init__(self, master=None):
Toplevel.__init__(self, master)
GraphDisplay.__init__(self)
self.protocol('WM_DELETE_WINDOW',self.WMDelete)
def Withdraw(self):
""" Withdraw window from screen.
"""
self.withdraw()
def WMDelete(self):
""" Window-Manager Quits only yield withdraws unless you quit
the AlgoWin. Override if you want group leader to handle
quit """
self.Withdraw()
def Show(self):
self.deiconify()
def SetTitle(self,title):
self.title(title)
""" *Internal* Given an event find the correspoding edge """
if not event.widget.find_withtag(CURRENT):
return None
else:
try:
widget = event.widget.find_withtag(CURRENT)[0]
e = self.edge[widget]
return e
except:
return NGato/GraphEditor.py 0100644 0017676 0017534 00000034016 10014153353 0015601 0 ustar 00schliep catbox 0000305 0050066 ################################################################################
#
# This file is part of Gato (Graph Animation Toolbox)
#
# file: GraphEditor.py
# author: Alexander Schliep (schliep@molgen.mpg.de)
#
# Copyright (C) 1998-2003, Alexander Schliep, Winfried Hochstaettler and
# ZAIK/ZPR, Universitaet zu Koeln
#
# Contact: schliep@molgen.mpg.de, wh@zpr.uni-koeln.de
#
# Information: http://gato.sf.net
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Library General Public
# License as published by the Free Software Foundation; either
# version 2 of the License, or (at your option) any later version.
#
# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Library General Public License for more details.
#
# You should have received a copy of the GNU Library General Public
# License along with this library; if not, write to the Free
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#
#
#
# This file is version $Revision: 1.16 $
# from $Date: 2003/03/07 15:16:16 $
# last change by $Author: buzdemir $.
#
################################################################################
from Tkinter import *
from Graph import Graph, Point2D
from math import sqrt
from GatoGlobals import *
from GraphDisplay import GraphDisplay
from tkSimpleDialog import askinteger, askfloat
import tkSimpleDialog
import string
import tkMessageBox
class EditWeightsDialog(tkSimpleDialog.Dialog):
""" Provide a dialog for editing vertex and edge weigths
- title the title in the dialog box
- nrOfWeights how many weights are there
- weights an array of initial values
- intFlag an array denoting whether the corresponding
entry is an integer (=1) or a float (=0)
Hack: A negative value will disable editing
- label an optional array of strings to use for weight names """
def __init__(self, master, title, nrOfWeights, weights, intFlag, label=None):
self.nrOfWeights = nrOfWeights
self.weights = weights
self.intFlag = intFlag
self.label = label
tkSimpleDialog.Dialog.__init__(self, master, title)
def body(self, master):
self.resizable(0,0)
#label = Label(master, text="Weight", anchor=W)
#label.grid(row=0, column=0, padx=4, pady=3)
label = Label(master, text="Value", anchor=W)
label.grid(row=0, column=1, padx=4, pady=3)
self.entry = [None] * self.nrOfWeights
for i in xrange(self.nrOfWeights):
if self.label == None:
label = Label(master, text="Weight %d" %(i+1), anchor=W)
else:
label = Label(master, text=self.label[i], anchor=W)
label.grid(row=i+1, column=0, padx=4, pady=3, sticky="e")
self.entry[i] = Entry(master, width=6, exportselection=FALSE)
if self.intFlag[i]:
self.entry[i].insert(0,"%d" % self.weights[i])
else:
self.entry[i].insert(0,"%f" % self.weights[i])
self.entry[i].grid(row=i+1, column=1, padx=4, pady=3, sticky="w")
def validate(self):
self.result = [None] * self.nrOfWeights
for i in xrange(self.nrOfWeights):
try:
if self.intFlag[i]:
self.result[i] = string.atoi(self.entry[i].get())
else:
self.result[i] = string.atof(self.entry[i].get())
except ValueError:
if self.intFlag[i]:
m = "Please enter an integer value for weight %d." % (i+1)
else:
m = "Please enter an floating point number for weight %d." % (i+1)
tkMessageBox.showwarning("Invalid Value", m, parent=self)
self.entry[i].selection_range(0,"end")
self.entry[i].focus_set()
self.result = None
return 0
return 1
class GraphEditor(GraphDisplay):
""" GraphEditor is a subclass of GraphDisplay providing an user interface
for editing options. Core edit operations are defined in GraphDisplay.
GraphEditor is not designed for direct consumption, use
- GraphEditorFrame
- GraphEditorToplevel
instead.
Bindings:
- Mouse, button 1 down/up: Add a vertex if nothing underneath mouse
else select for move vertex
- Mouse, move: move vertex
- Mouse, button 2 down: select tail for adding an edge
- Mouse, button 2 up: select head for adding an edge
- Mouse, button 3 up: delete vertex/edge underneath mouse
"""
def __init__(self):
GraphDisplay.__init__(self)
self.rubberbandLine = None
self.movedVertex = None
self.startx = None # position where MouseDown first occurred
self.starty = None
self.gridSize = gGridSize
self.gridding = 0
self.mode = 'AddOrMoveVertex'
# 'AddEdge' 'DeleteEdgeOrVertex' 'SwapOrientation' 'EditWeight'
def ToggleGridding(self):
""" Toggle gridding """
if self.gridding:
self.gridding = 0
else:
self.gridding = 1
def SetEditMode(self,mode):
self.mode = mode
def WindowToCanvasCoords(self,event):
""" Given an event return the (x,y) in canvas coordinates while
using gridding if a gridsize is specified in gGridSize """
if not self.gridding:
x = self.canvas.canvasx(event.x)
y = self.canvas.canvasy(event.y)
else:
x = self.canvas.canvasx(event.x,self.gridSize)
y = self.canvas.canvasy(event.y,self.gridSize)
return (x,y)
def Zoom(self,percent):
try:
GraphDisplay.Zoom(self,percent)
self.gridSize = (gGridSize * self.zoomFactor) / 100.0
except:
return None
def CreateWidgets(self):
""" Add additional bindings with proper callbacks to canvas """
GraphDisplay.CreateWidgets(self)
Widget.bind(self.canvas, "", self.MouseMotion)
Widget.bind(self.canvas, "<1>", self.MouseDown)
Widget.bind(self.canvas, "", self.MouseMove)
Widget.bind(self.canvas, "", self.MouseUp)
Widget.bind(self.canvas, "<2>", self.Mouse2Down)
Widget.bind(self.canvas, "", self.Mouse2Move)
Widget.bind(self.canvas, "", self.Mouse2Up)
Widget.bind(self.canvas, "", self.Mouse3Up)
Widget.bind(self.canvas, "", self.CanvasEnter)
Widget.bind(self.canvas, "", self.CanvasLeave)
#===== ACTIONS ==============================================================
def ShowCoords(self,event):
x,y = self.WindowToCanvasCoords(event)
v=self.FindVertex(event)
if v == None and self.gridding:
v = self.FindGridVertex(event)
e = self.FindEdge(event)
if e!=None:
infoString = "Edge (%d,%d)" % (e[0], e[1])
elif v!=None:
infoString = "Vertex %d at position (%d,%d)" % (v,
self.embedding[v].x,
self.embedding[v].y)
elif x>=0 and y>=0:
x,y = self.CanvasToEmbedding(x,y)
infoString = "(%d,%d)" % (x,y)
else:
infoString = ""
self.UpdateInfo(infoString)
def AddOrMoveVertexDown(self,event):
v = self.FindVertex(event)
if v == None:
if self.FindGridVertex(event) == None:
x,y = self.WindowToCanvasCoords(event)
x = max(x,0)
y = max(y,0)
self.AddVertexCanvas(x,y)
self.movedVertex = None
else:
self.canvas.addtag("mySel", "withtag", self.drawVertex[v])
self.canvas.addtag("mySel", "withtag", self.drawLabel[v])
try:
self.canvas.addtag("mySel", "withtag", self.drawEdges[(v,v)])
except:
pass
self.canvas.lift("mySel")
# We want to start off with user clicking smack in middle of
# vertex -- cant force him, so we fake it
c = self.canvas.coords(self.drawVertex[v])
# c already canvas coordinates
self.oldx = (c[2] - c[0])/2 + c[0]
self.oldy = (c[3] - c[1])/2 + c[1]
self.movedVertex = v
self.didMoveVertex = 0
def AddOrMoveVertexMove(self,event):
if not self.canvasleft:
self.newx,self.newy = self.WindowToCanvasCoords(event)
self.newx = max(self.newx,0)
self.newy = max(self.newy,0)
self.update_idletasks()
try:
self.canvas.lift("mySel")
self.canvas.move("mySel",
self.newx - self.oldx,
self.newy - self.oldy)
#self.MoveVertex(self.movedVertex,self.newx,self.newy)
self.oldx = self.newx
self.oldy = self.newy
x,y = self.CanvasToEmbedding(self.newx,self.newy)
infoString = "Vertex %d at position (%d,%d)" % (self.movedVertex,x,y)
self.UpdateInfo(infoString)
self.didMoveVertex = 1
except:
i = 1 # Need instruction after except
def AddOrMoveVertexUp(self,event):
if self.movedVertex != None:
# Moving within vertex oval does not move vertex
self.update_idletasks()
if self.didMoveVertex:
self.MoveVertex(self.movedVertex,self.newx,self.newy)
self.movedVertex = None
self.canvas.dtag("mySel")
def AddEdgeDown(self,event):
self.tail = self.FindVertex(event)
if self.tail != None:
c = self.canvas.coords(self.drawVertex[self.tail])
self.startx = (c[2] - c[0])/2 + c[0]
self.starty = (c[3] - c[1])/2 + c[1]
def AddEdgeMove(self,event):
if self.tail != None:
# canvas x and y take the screen coords from the event and translate
# them into the coordinate system of the canvas object
x = self.canvas.canvasx(event.x)
y = self.canvas.canvasy(event.y)
if (self.startx != event.x) and (self.starty != event.y) :
self.canvas.delete(self.rubberbandLine)
self.rubberbandLine = self.canvas.create_line(
self.startx, self.starty, x, y)
self.canvas.lower(self.rubberbandLine,"vertices")
# this flushes the output, making sure that
# the rectangle makes it to the screen
# before the next event is handled
self.update_idletasks()
def AddEdgeUp(self,event):
if self.rubberbandLine != None:
self.canvas.delete(self.rubberbandLine)
if self.tail != None:
x = self.canvas.canvasx(event.x)
y = self.canvas.canvasy(event.y)
widget = event.widget.find_closest(x,y,None,self.rubberbandLine)
if widget:
widget = widget[0]
tags = self.canvas.gettags(widget)
head = None
if "vertices" in tags:
head = self.vertex[widget]
elif "labels" in tags:
head = self.label[widget]
if head != None:
self.AddEdge(self.tail,head)
def DeleteEdgeOrVertexUp(self,event):
if event.widget.find_withtag(CURRENT):
widget = event.widget.find_withtag(CURRENT)[0]
tags = self.canvas.gettags(widget)
if "edges" in tags:
(tail,head) = self.edge[widget]
self.DeleteEdge(tail,head)
else:
if "vertices" in tags:
v = self.vertex[widget]
elif "labels" in tags:
v = self.label[widget]
self.DeleteVertex(v)
self.tail = None
if self.rubberbandLine != None:
self.canvas.delete(self.rubberbandLine)
def SwapOrientationUp(self,event):
if event.widget.find_withtag(CURRENT):
widget = event.widget.find_withtag(CURRENT)[0]
tags = self.canvas.gettags(widget)
if "edges" in tags:
(tail,head) = self.edge[widget]
self.SwapEdgeOrientation(tail,head)
def EditWeightUp(self,event):
if event.widget.find_withtag(CURRENT):
widget = event.widget.find_withtag(CURRENT)[0]
tags = self.canvas.gettags(widget)
if "edges" in tags:
(tail,head) = self.edge[widget]
weights = ()
intFlag = ()
count = len(self.G.edgeWeights.keys())
for i in xrange(count):
weights = weights + (self.G.edgeWeights[i][(tail,head)],)
intFlag = intFlag + (self.G.edgeWeights[i].QInteger(),)
d = EditWeightsDialog(self, "Edit edge weights (%d,%d)" % (tail,head),
count, weights, intFlag)
if d.result is not None:
for i in xrange(count):
self.G.edgeWeights[i][(tail,head)] = d.result[i]
else: # We have a vertex
v = self.FindVertex(event)
if v != None and self.G.NrOfVertexWeights() > 0:
weights = ()
intFlag = ()
count = len(self.G.vertexWeights.keys())
for i in xrange(count):
weights = weights + (self.G.vertexWeights[i][v],)
intFlag = intFlag + (self.G.vertexWeights[i].QInteger(),)
d = EditWeightsDialog(self, "Edit vertex weights %d" % v,
count, weights, intFlag)
if d.result is not None:
for i in xrange(count):
self.G.vertexWeights[i][v] = d.result[i]
#===== GUI-Bindings FOR ACTIONS ================================================
def MouseMotion(self,event):
if self.mode == 'AddOrMoveVertex':
self.ShowCoords(event)
def MouseDown(self,event):
if self.mode == 'AddOrMoveVertex':
self.AddOrMoveVertexDown(event)
elif self.mode == 'AddEdge':
self.AddEdgeDown(event)
def MouseMove(self,event):
if self.mode == 'AddOrMoveVertex':
self.AddOrMoveVertexMove(event)
elif self.mode == 'AddEdge':
self.AddEdgeMove(event)
def MouseUp(self,event):
if self.mode == 'AddOrMoveVertex':
self.AddOrMoveVertexUp(event)
elif self.mode == 'AddEdge':
self.AddEdgeUp(event)
elif self.mode == 'DeleteEdgeOrVertex':
self.DeleteEdgeOrVertexUp(event)
elif self.mode == 'SwapOrientation':
self.SwapOrientationUp(event)
elif self.mode == 'EditWeight':
self.EditWeightUp(event)
def Mouse2Down(self,event):
self.AddEdgeDown(event)
def Mouse2Move(self,event):
self.AddEdgeMove(event)
def Mouse2Up(self,event):
self.AddEdgeUp(event)
def Mouse3Up(self,event):
self.DeleteEdgeOrVertexUp(event)
def CanvasEnter(self,event):
self.canvasleft = 0
def CanvasLeave(self, event):
self.canvasleft = 1
class GraphEditorFrame(GraphEditor, Frame):
""" A GraphEditor in a frame """
def __init__(self, master=None):
Frame.__init__(self, master)
self.pack()
self.pack(expand=1,fill=BOTH) # Makes whole window resizeable
GraphEditor.__init__(self)
def SetTitle(self,title):
sys.info("change window title to %s" % title)
class GraphEditorToplevel(GraphEditor, Toplevel):
""" A GraphEditor in a top-level window """
def __init__(self, master=None):
Toplevel.__init__(self, master)
GraphEditor.__init__(self)
def SetTitle(self,title):
self.title(title)
. Core edit operations are defined in GraphDisplay.
GraphEditor is not designed for direct consumption, use
- GraphEditorFrame
- GraphEditorToplevel
instead.
Bindings:
- Mouse, button 1 down/up: Add a vertex if nothing underneath mouse
else select for move vertex
- Mouse, move: move vertex
- Mouse, button 2 down: select tail for adding an edge
- Mouse, button 2 up: select head for adding an edge
- Mouse, button 3 up: delete vertex/edge underneath mouse
"""
def __iGato/GraphUtil.py 0100644 0017676 0017534 00000032552 10014153354 0015274 0 ustar 00schliep catbox 0000305 0050066 ################################################################################
#
# This file is part of Gato (Graph Animation Toolbox)
#
# file: Graph.py
# author: Alexander Schliep (schliep@molgen.mpg.de)
#
# Copyright (C) 1998-2003, Alexander Schliep, Winfried Hochstaettler and
# ZAIK/ZPR, Universitaet zu Koeln
#
# Contact: schliep@molgen.mpg.de, wh@zpr.uni-koeln.de
#
# Information: http://gato.sf.net
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Library General Public
# License as published by the Free Software Foundation; either
# version 2 of the License, or (at your option) any later version.
#
# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Library General Public License for more details.
#
# You should have received a copy of the GNU Library General Public
# License along with this library; if not, write to the Free
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#
#
#
# This file is version $Revision: 1.18 $
# from $Date: 2003/10/25 09:54:33 $
# last change by $Author: schliep $.
#
################################################################################
import types
import StringIO
from string import split
from GatoGlobals import *
from Graph import Graph
from DataStructures import Point2D, VertexLabeling, EdgeLabeling, EdgeWeight, VertexWeight, Queue
import logging
log = logging.getLogger("GraphUtil.py")
################################################################################
#
# Syntactic Sugar
#
################################################################################
def Vertices(G):
""" Returns the vertices of G. Hide method call """
return G.vertices
def Edges(G):
""" Returns the edges of G. Hide method call """
return G.Edges()
################################################################################
#
# Basic algorithms
#
################################################################################
def BFS(G,root,direction='forward'):
""" Calculate BFS distances and predecessor without showing animations.
If G is directed, direction does matter:
- 'forward' BFS will use outgoing edges
- 'backward' BFS will use incoming edges
It uses gInfinity (from GatoGlobals.py) as infinite distance.
returns (dist,pred) """
Q = Queue()
d = {}
pred = {}
for v in G.vertices:
d[v] = gInfinity
d[root] = 0
pred[root] = root
Q.Append(root)
while Q.IsNotEmpty():
v = Q.Top()
if G.QDirected() == 1 and direction == 'backward':
nbh = G.InNeighbors(v)
else:
nbh = G.Neighborhood(v)
for w in nbh:
if d[w] == gInfinity:
d[w] = d[v] + 1
pred[w] = v
Q.Append(w)
return (d,pred)
def ConnectedComponents(G):
""" Compute the connected components of the undirected graph G.
Returns a list of lists of vertices. """
result = []
visited = {}
for v in G.vertices:
visited[v] = None
for root in G.vertices:
if visited[root] is not None:
continue
else: # Found a new component
component = [root]
visited[root] = 1
Q = Queue()
Q.Append(root)
while Q.IsNotEmpty():
v = Q.Top()
nbh = G.Neighborhood(v)
for w in nbh:
if visited[w] == None:
visited[w] = 1
Q.Append(w)
component.append(w)
result.append(component)
return result
################################################################################
#
# GraphInformer
#
################################################################################
class GraphInformer:
""" Provides information about edges and vertices of a graph.
Used as argument for GraphDisplay.RegisterGraphInformer() """
def __init__(self,G):
self.G = G
self.info = ""
def DefaultInfo(self):
""" Provide an default text which is shown when no edge/vertex
info is displayed """
return self.info
def VertexInfo(self,v):
""" Provide an info text for vertex v """
return "Vertex %d at position (%d,%d)" % (v,
self.G.embedding[v].x,
self.G.embedding[v].y)
def EdgeInfo(self,tail,head):
""" Provide an info text for edge (tail,head) """
return "Edge (%d,%d)" % (tail, head)
def SetDefaultInfo(self, info=""):
self.info = info
class WeightedGraphInformer(GraphInformer):
""" Provides information about weighted edges and vertices of a graph.
Used as argument for GraphDisplay.RegisterGraphInformer() """
def __init__(self,G,weightDesc="weight"):
""" G is the graph we want to supply information about and weightDesc
a textual interpretation of the weight """
GraphInformer.__init__(self,G)
self.weightDesc = weightDesc
def EdgeInfo(self,tail,head):
""" Provide an info text for weighted edge (tail,head) """
# How to handle undirected graph
if self.G.QDirected() == 0:
try:
w = self.G.edgeWeights[0][(tail, head)]
except KeyError:
w = self.G.edgeWeights[0][(head, tail)]
else:
w = self.G.edgeWeights[0][(tail, head)]
if self.G.edgeWeights[0].QInteger():
return "Edge (%d,%d) %s: %d" % (tail, head, self.weightDesc, w)
else:
return "Edge (%d,%d) %s: %f" % (tail, head, self.weightDesc, w)
class MSTGraphInformer(WeightedGraphInformer):
def __init__(self,G,T):
WeightedGraphInformer.__init__(self,G)
self.T = T
def DefaultInfo(self):
""" Provide an default text which is shown when no edge/vertex
info is displayed """
return "Tree has %d vertices and weight %5.2f" % (self.T.Order(),self.T.Weight())
class FlowGraphInformer(GraphInformer):
def __init__(self,G,flow):
GraphInformer.__init__(self,G)
self.flow = flow
self.cap = flow.cap
self.res = flow.res
self.excess = flow.excess
def EdgeInfo(self,v,w):
return "Edge (%d,%d) - flow: %d of %d" % (v,w, self.flow[(v,w)], self.cap[(v,w)])
def VertexInfo(self,v):
tmp = self.excess[v]
if tmp == gInfinity:
str1 = "Infinity"
elif tmp == -gInfinity:
str1 = "-Infinity"
else:
str1 = "%d"%tmp
return "Vertex %d - excess: %s" % (v, str1)
class ResidualGraphInformer(FlowGraphInformer):
def EdgeInfo(self,v,w):
return "Edge (%d,%d) - residual capacity: %d" % (v, w, self.res[(v,w)])
################################################################################
#
# FILE I/O
#
################################################################################
def OpenCATBoxGraph(_file):
""" Reads in a graph from file fileName. File-format is supposed
to be from old CATBOX++ (*.cat) """
G = Graph()
E = VertexLabeling()
W = EdgeWeight(G)
L = VertexLabeling()
# get file from name or file object
graphFile=None
if type(_file) in types.StringTypes:
graphFile = open(_file, 'r')
elif type(_file)==types.FileType or issubclass(_file.__class__,StringIO.StringIO):
graphFile=_file
else:
raise Exception("got wrong argument")
lineNr = 1
firstVertexLineNr = -1
lastVertexLineNr = -1
firstEdgeLineNr = -1
lastEdgeLineNr = -1
intWeights = 0
while 1:
line = graphFile.readline()
if not line:
break
if lineNr == 2: # Read directed and euclidian
splitLine = split(line[:-1],';')
G.directed = eval(split(splitLine[0],':')[1])
G.simple = eval(split(splitLine[1],':')[1])
G.euclidian = eval(split(splitLine[2],':')[1])
intWeights = eval(split(splitLine[3],':')[1])
nrOfEdgeWeights = eval(split(splitLine[4],':')[1])
nrOfVertexWeights = eval(split(splitLine[5],':')[1])
for i in xrange(nrOfEdgeWeights):
G.edgeWeights[i] = EdgeWeight(G)
for i in xrange(nrOfVertexWeights):
G.vertexWeights[i] = VertexWeight(G)
if lineNr == 5: # Read nr of vertices
nrOfVertices = eval(split(line[:-2],':')[1]) # Strip of "\n" and ;
firstVertexLineNr = lineNr + 1
lastVertexLineNr = lineNr + nrOfVertices
if firstVertexLineNr <= lineNr and lineNr <= lastVertexLineNr:
splitLine = split(line[:-1],';')
v = G.AddVertex()
x = eval(split(splitLine[1],':')[1])
y = eval(split(splitLine[2],':')[1])
for i in xrange(nrOfVertexWeights):
w = eval(split(splitLine[3+i],':')[1])
G.vertexWeights[i][v] = w
E[v] = Point2D(x,y)
if lineNr == lastVertexLineNr + 1: # Read Nr of edges
nrOfEdges = eval(split(line[:-2],':')[1]) # Strip of "\n" and ;
firstEdgeLineNr = lineNr + 1
lastEdgeLineNr = lineNr + nrOfEdges
if firstEdgeLineNr <= lineNr and lineNr <= lastEdgeLineNr:
splitLine = split(line[:-1],';')
h = eval(split(splitLine[0],':')[1])
t = eval(split(splitLine[1],':')[1])
G.AddEdge(t,h)
for i in xrange(nrOfEdgeWeights):
G.edgeWeights[i][(t,h)] = eval(split(splitLine[3+i],':')[1])
lineNr = lineNr + 1
graphFile.close()
for v in G.vertices:
L[v] = v
G.embedding = E
G.labeling = L
if intWeights:
G.Integerize('all')
for i in xrange(nrOfVertexWeights):
G.vertexWeights[i].Integerize()
return G
def SaveCATBoxGraph(G, _file):
""" Save graph to file fileName in file-format from old CATBOX++ (*.cat) """
# get file from name or file object
file=None
if type(_file) in types.StringTypes:
file = open(_file, 'w')
elif type(_file)==types.FileType or issubclass(_file.__class__,StringIO.StringIO):
file=_file
else:
raise Exception("got wrong argument")
nrOfVertexWeights = len(G.vertexWeights.keys())
nrOfEdgeWeights = len(G.edgeWeights.keys())
integerEdgeWeights = G.edgeWeights[0].QInteger()
file.write("graph:\n")
file.write("dir:%d; simp:%d; eucl:%d; int:%d; ew:%d; vw:%d;\n" %
(G.QDirected(), G.simple, G.QEuclidian(), integerEdgeWeights,
nrOfEdgeWeights, nrOfVertexWeights))
file.write("scroller:\n")
file.write("vdim:1000; hdim:1000; vlinc:10; hlinc:10; vpinc:50; hpinc:50;\n")
file.write("vertices:" + `G.Order()` + ";\n")
# Force continous numbering of vertices
count = 1
save = {}
for v in G.vertices:
save[v] = count
count = count + 1
file.write("n:%d; x:%d; y:%d;" % (save[v], G.embedding[v].x, G.embedding[v].y))
for i in xrange(nrOfVertexWeights):
if integerEdgeWeights: # XXX
file.write(" w:%d;" % int(round(G.vertexWeights[i][v])))
else:
file.write(" w:%d;" % G.vertexWeights[i][v])
file.write("\n")
file.write("edges:" + `G.Size()` + ";\n")
for tail in G.vertices:
for head in G.OutNeighbors(tail):
file.write("h:%d; t:%d; e:2;" % (save[head], save[tail]))
for i in xrange(nrOfEdgeWeights):
if integerEdgeWeights:
file.write(" w:%d;" % int(round(G.edgeWeights[i][(tail,head)])))
else:
file.write(" w:%f;" % G.edgeWeights[i][(tail,head)])
file.write("\n")
#### GML
def ParseGML(file):
retval = []
while 1:
line = file.readline()
if not line:
return retval
token = filter(lambda x: x != '', split(line[:-1],"[\t ]*"))
if len(token) == 1 and token[0] == ']':
return retval
elif len(token) == 2:
if token[1] == '[':
retval.append((token[0], ParseGML(file)))
else:
retval.append((token[0], token[1]))
else:
log.error("Serious format error line %s:" % line)
def PairListToDictionary(l):
d = {}
for i in xrange(len(l)):
d[l[i][0]] = l[i][1]
return d
def OpenGMLGraph(fileName):
""" Reads in a graph from file fileName. File-format is supposed
to be GML (*.gml) """
G = Graph()
G.directed = 0
E = VertexLabeling()
W = EdgeWeight(G)
L = VertexLabeling()
VLabel = VertexLabeling()
ELabel = EdgeLabeling()
file = open(fileName, 'r')
g = ParseGML(file)
file.close()
if g[0][0] != 'graph':
log.error("Serious format error in %s. first key is not graph" % fileName)
return
else:
l = g[0][1]
for i in xrange(len(l)):
key = l[i][0]
value = l[i][1]
if key == 'node':
d = PairListToDictionary(value)
v = G.AddVertex()
try:
VLabel[v] = eval(d['label'])
P = PairListToDictionary(d['graphics'])
E[v] = Point2D(eval(P['x']), eval(P['y']))
except:
d = None
P = None
elif key == 'edge':
d = PairListToDictionary(value)
try:
s = eval(d['source'])
t = eval(d['target'])
G.AddEdge(s,t)
ELabel[(s,t)] = eval(d['label'])
W[(s,t)] = 0
except:
d = None
elif key == 'directed':
G.directed = 1
for v in G.vertices:
L[v] = v
G.embedding = E
G.labeling = L
G.nrEdgeWeights = 1
G.edgeWeights[0] = W
G.vertexAnnotation = VLabel
G.edgeAnnotation = ELabel
return G
n when no edge/vertex
info is displayed """
return self.info
def VertexInfo(self,v):
""" Provide an info text for vertex Gato/Gred.py 0100755 0017676 0017534 00000066443 10014153354 0014267 0 ustar 00schliep catbox 0000305 0050066 #!/usr/bin/env python2.2
################################################################################
#
# This file is part of Gato (Graph Animation Toolbox)
#
# file: Gred.py
# author: Alexander Schliep (schliep@molgen.mpg.de)
#
# Copyright (C) 1998-2003, Alexander Schliep, Winfried Hochstaettler and
# ZAIK/ZPR, Universitaet zu Koeln
#
# Contact: schliep@molgen.mpg.de, wh@zpr.uni-koeln.de
#
# Information: http://gato.sf.net
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Library General Public
# License as published by the Free Software Foundation; either
# version 2 of the License, or (at your option) any later version.
#
# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Library General Public License for more details.
#
# You should have received a copy of the GNU Library General Public
# License along with this library; if not, write to the Free
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#
#
#
# This file is version $Revision: 1.29 $
# from $Date: 2003/06/03 20:09:50 $
# last change by $Author: schliep $.
#
################################################################################
from GatoGlobals import *
import GatoGlobals # Needed for help viewer.XXX
from Graph import Graph
from DataStructures import EdgeWeight, VertexWeight
from GraphUtil import OpenCATBoxGraph, OpenGMLGraph, SaveCATBoxGraph, WeightedGraphInformer
from GraphEditor import GraphEditor
from Tkinter import *
import tkFont
from GatoUtil import stripPath, extension, gatoPath
import GatoDialogs
import GatoIcons
from ScrolledText import *
from tkFileDialog import askopenfilename, asksaveasfilename
from tkMessageBox import askokcancel
import tkSimpleDialog
import whrandom
import string
import sys
import os
import GraphCreator, Embedder
class GredSplashScreen(GatoDialogs.SplashScreen):
def CreateWidgets(self):
self.Icon = PhotoImage(data=GatoIcons.gred)
self.label = Label(self, image=self.Icon)
self.label.pack(side=TOP)
self.label = Label(self, text=GatoDialogs.crnotice1)
self.label.pack(side=TOP)
label = Label(self, font="Helvetica 10", text=GatoDialogs.crnotice2, justify=CENTER)
label.pack(side=TOP)
class GredAboutBox(GatoDialogs.AboutBox):
def body(self, master):
self.resizable(0,0)
self.catIconImage = PhotoImage(data=GatoIcons.gred)
self.catIcon = Label(master, image=self.catIconImage)
self.catIcon.pack(side=TOP)
label = Label(master, text=GatoDialogs.crnotice1)
label.pack(side=TOP)
label = Label(master, font="Helvetica 10",
text=GatoDialogs.crnotice2, justify=CENTER)
label.pack(side=TOP)
color = self.config("bg")[4]
self.infoText = ScrolledText(master, relief=FLAT,
padx=3, pady=3,
background=color,
#foreground="black",
wrap='word',
width=60, height=12,
font="Times 10")
self.infoText.pack(expand=0, fill=X, side=BOTTOM)
self.infoText.delete('0.0', END)
self.infoText.insert('0.0', GatoGlobals.gLGPLText)
self.infoText.configure(state=DISABLED)
self.title("Gred - About")
class RandomizeEdgeWeightsDialog(tkSimpleDialog.Dialog):
""" self.result is an array of triples (randomize, min, max)
where 'randomize' indicates whether to randomize weight i
and min and max give the range the random values are drawn
from.
If user cancelled, self.result is None """
def __init__(self, master, nrOfWeights, keepFirst):
self.keepFirst = keepFirst
self.nrOfWeights = nrOfWeights
tkSimpleDialog.Dialog.__init__(self, master, "Randomize Edge Weights")
def body(self, master):
self.resizable(0,0)
label = Label(master, text="Weight", anchor=W)
label.grid(row=0, column=0, padx=4, pady=3, sticky="e")
label = Label(master, text="Randomize", anchor=W)
label.grid(row=0, column=1, padx=4, pady=3, sticky="e")
label = Label(master, text="Minimum", anchor=W)
label.grid(row=0, column=2, padx=4, pady=3, sticky="e")
label = Label(master, text="Maximum", anchor=W)
label.grid(row=0, column=3, padx=4, pady=3, sticky="e")
self.minimum = []
self.maximum = []
self.check = []
self.checkVar = []
for i in xrange(self.nrOfWeights):
label = Label(master, text="%d" % (i+1), anchor=W)
label.grid(row=i+1, column=0, padx=4, pady=3, sticky="e")
if (i == 0 and not self.keepFirst) or i > 0:
self.checkVar.append(IntVar())
self.check.append(Checkbutton(master,
variable=self.checkVar[i]))
self.check[i].select()
self.check[i].grid(row=i+1, column=1, padx=4, pady=3, sticky="e")
self.minimum.append(Entry(master, width=6, exportselection=FALSE))
self.minimum[i].insert(0,"0")
self.minimum[i].grid(row=i+1, column=2, padx=4, pady=3, sticky="e")
self.maximum.append(Entry(master, width=6, exportselection=FALSE))
self.maximum[i].insert(0,"100")
self.maximum[i].grid(row=i+1, column=3, padx=4, pady=3, sticky="e")
else:
self.checkVar.append(None)
self.check.append(None)
self.minimum.append(None)
self.maximum.append(None)
def validate(self):
self.result = []
for i in xrange(self.nrOfWeights):
if self.checkVar[i] != None:
self.result.append( (self.checkVar[i].get(),
string.atof(self.minimum[i].get()),
string.atof(self.maximum[i].get())))
else:
self.result.append( (0, None, None))
# try:
# minimun = string.atof(self.minimum[i].get())
# except ValueError:
# minimum = "Please enter an floating point number for minimum of weight %d." % (i+1)
# try:
# maximum = string.atof(self.maximum[i].get())
# except ValueError:
# m = "Please enter an floating point number for maximum of weight %d." % (i+1)
# try:
# maximum = string.atof(self.maximum[i].get())
# except ValueError:
# m = "Please enter an floating point number for maximum of weight %d." % (i+1)
return 1
class SAGraphEditor(GraphEditor, Frame):
def __init__(self, master=None):
Frame.__init__(self, master)
Splash = GredSplashScreen(self.master)
self.G = None
self.pack()
self.pack(expand=1,fill=BOTH) # Makes whole window resizeable
self.makeMenuBar()
GraphEditor.__init__(self)
self.fileName = None
self.dirty = 0
#self.zoomMenu['state'] = DISABLED
self.SetGraphMenuOptions()
Splash.Destroy()
# Fix focus and stacking
if os.name == 'nt' or os.name == 'dos':
self.master.tkraise()
self.master.focus_force()
else:
self.tkraise()
def ReadConfiguration(self):
self.gVertexRadius = 13
self.gEdgeWidth = 3
self.gFontFamily = "Helvetica"
self.gFontSize = 11
self.gFontStyle = tkFont.BOLD
self.gVertexFrameWidth = 0
self.cVertexDefault = "#000099"
self.cVertexBlink = "black"
self.cEdgeDefault = "#999999"
self.cLabelDefault = "white"
self.cLabelDefaultInverted = "black"
self.cLabelBlink = "green"
# Used by ramazan's scaling code
self.zVertexRadius = self.gVertexRadius
self.zArrowShape = (16, 20, 6)
self.zFontSize = 10
def SetGraphMenuDirected(self,directed):
if directed:
if not self.directedVar.get():
self.graphMenu.invoke(self.graphMenu.index('Directed'))
else:
if self.directedVar.get():
self.graphMenu.invoke(self.graphMenu.index('Directed'))
def SetGraphMenuEuclidean(self,euclidean):
if euclidean:
if not self.euclideanVar.get():
self.graphMenu.invoke(self.graphMenu.index('Euclidean'))
else:
if self.euclideanVar.get():
self.graphMenu.invoke(self.graphMenu.index('Euclidean'))
def SetGraphMenuIntegerVertexWeights(self,IntegerVertexWeights):
if IntegerVertexWeights:
if not self.vertexIntegerWeightsVar.get():
self.graphMenu.invoke(self.graphMenu.
index('Integer Vertex Weights'))
else:
if self.vertexIntegerWeightsVar.get():
self.graphMenu.invoke(self.graphMenu.
index('Integer Vertex Weights'))
def SetGraphMenuVertexWeights(self,VertexWeights):
self.vertexWeightsSubmenu.invoke(self.vertexWeightsSubmenu.index(VertexWeights))
def SetGraphMenuIntegerEdgeWeights(self,IntegerEdgeWeights):
if IntegerEdgeWeights:
if not self.edgeIntegerWeightsVar.get():
self.graphMenu.invoke(self.graphMenu.
index('Integer Edge Weights'))
else:
if self.edgeIntegerWeightsVar.get():
self.graphMenu.invoke(self.graphMenu.
index('Integer Edge Weights'))
def SetGraphMenuEdgeWeights(self,EdgeWeights):
self.edgeWeightsSubmenu.invoke(self.edgeWeightsSubmenu.index(EdgeWeights))
def SetGraphMenuGrid(self,Grid):
if Grid:
if not self.gridding:
self.graphMenu.invoke(self.graphMenu.index('Grid'))
else:
if self.gridding:
self.graphMenu.invoke(self.graphMenu.index('Grid'))
def SetGraphMenuOptions(self):
self.SetGraphMenuDirected(1)
self.SetGraphMenuEuclidean(1)
self.SetGraphMenuGrid(1)
self.defaultButton.select()
#self.toolVar.set('Add or move vertex')
self.SetGraphMenuIntegerVertexWeights(0)
self.SetGraphMenuVertexWeights('None')
self.SetGraphMenuIntegerEdgeWeights(0)
self.SetGraphMenuEdgeWeights('One')
def SetTitle(self,title):
self.master.title(title)
def CreateWidgets(self):
toolbar = Frame(self, cursor='hand2', relief=FLAT)
toolbar.pack(side=LEFT, fill=Y) # Allows horizontal growth
extra = Frame(toolbar, cursor='hand2', relief=SUNKEN, borderwidth=2)
extra.pack(side=TOP) # Allows horizontal growth
extra.rowconfigure(5,weight=1)
extra.bind("", lambda e, gd=self:gd.DefaultInfo())
px = 0
py = 3
self.toolVar = StringVar()
self.lastTool = None
# Load Icons
# 0 = "inactive", 1 = "mouse over", 2 = "active"
self.icons = {
'AddOrMoveVertex':[PhotoImage(data=GatoIcons.vertex_1),
PhotoImage(data=GatoIcons.vertex_2),
PhotoImage(data=GatoIcons.vertex_3)],
'AddEdge':[PhotoImage(data=GatoIcons.edge_1),
PhotoImage(data=GatoIcons.edge_2),
PhotoImage(data=GatoIcons.edge_3)],
'DeleteEdgeOrVertex':[PhotoImage(data=GatoIcons.delete_1),
PhotoImage(data=GatoIcons.delete_2),
PhotoImage(data=GatoIcons.delete_3)],
'SwapOrientation':[PhotoImage(data=GatoIcons.swap_1),
PhotoImage(data=GatoIcons.swap_2),
PhotoImage(data=GatoIcons.swap_3)],
'EditWeight':[PhotoImage(data=GatoIcons.edit_1),
PhotoImage(data=GatoIcons.edit_2),
PhotoImage(data=GatoIcons.edit_3)] }
self.buttons = {}
values = ['AddOrMoveVertex','AddEdge','DeleteEdgeOrVertex',
'SwapOrientation','EditWeight']
text = {'AddOrMoveVertex':'Add or move vertex','AddEdge':'Add edge',
'DeleteEdgeOrVertex':'Delete edge or vertex',
'SwapOrientation':'Swap orientation','EditWeight':'Edit Weight'}
row = 0
for val in values:
b = Radiobutton(extra, width=32, padx=px, pady=py,
text=text[val],
command=self.ChangeTool,
var = self.toolVar, value=val,
indicator=0, image=self.icons[val][0],
selectcolor="#AFAFAF",)
b.grid(row=row, column=0, padx=2, pady=2)
self.buttons[val] = b
b.bind("", lambda e,gd=self:gd.EnterButtonCallback(e))
b.bind("", lambda e,gd=self:gd.LeaveButtonCallback(e))
row += 1
self.defaultButton = self.buttons['AddOrMoveVertex']
# default doesnt work as config option
GraphEditor.CreateWidgets(self)
def EnterButtonCallback(self,e):
w = e.widget
text = string.join(w.config("text")[4])
self.UpdateInfo(text)
value = w.config("value")[4]
w.configure(image=self.icons[value][1])
def LeaveButtonCallback(self,e):
self.UpdateInfo("")
w = e.widget
value = w.config("value")[4]
if self.toolVar.get() == value: # the button we are leaving is depressed
w.configure(image=self.icons[value][2])
else:
w.configure(image=self.icons[value][0])
def makeMenuBar(self, toplevel=0):
self.menubar = Menu(self,tearoff=0)
# Add file menu
self.fileMenu = Menu(self.menubar, tearoff=0)
self.fileMenu.add_command(label='New', command=self.NewGraph)
self.fileMenu.add_command(label='Open ...', command=self.OpenGraph)
self.fileMenu.add_command(label='Save', command=self.SaveGraph)
self.fileMenu.add_command(label='Save as ...', command=self.SaveAsGraph)
self.fileMenu.add_separator()
self.fileMenu.add_command(label='Export EPSF...', command=self.ExportEPSF)
self.fileMenu.add_separator()
self.fileMenu.add_command(label='Quit', command=self.Quit)
self.menubar.add_cascade(label="File", menu=self.fileMenu,
underline=0)
# Add graph menu
self.graphMenu = Menu(self.menubar, tearoff=0)
self.directedVar = IntVar()
self.graphMenu.add_checkbutton(label='Directed',
command=self.graphDirected,
var = self.directedVar)
self.euclideanVar = IntVar()
self.graphMenu.add_checkbutton(label='Euclidean',
command=self.graphEuclidean,
var = self.euclideanVar)
self.graphMenu.add_separator()
# vertex weigths
self.vertexIntegerWeightsVar = IntVar()
self.graphMenu.add_checkbutton(label='Integer Vertex Weights',
command=self.vertexIntegerWeights,
var = self.vertexIntegerWeightsVar)
self.vertexWeightsSubmenu = Menu(self.graphMenu, tearoff=0)
self.vertexWeightVar = IntVar()
self.vertexWeightsSubmenu.add_radiobutton(label="None",
command=self.ChangeVertexWeights,
var = self.vertexWeightVar, value=0)
self.vertexWeightsSubmenu.add_radiobutton(label="One",
command=self.ChangeVertexWeights,
var = self.vertexWeightVar, value=1)
self.vertexWeightsSubmenu.add_radiobutton(label="Two",
command=self.ChangeVertexWeights,
var = self.vertexWeightVar, value=2)
self.vertexWeightsSubmenu.add_radiobutton(label="Three",
command=self.ChangeVertexWeights,
var = self.vertexWeightVar, value=3)
self.graphMenu.add_cascade(label='Vertex Weights',
menu=self.vertexWeightsSubmenu)
# edge weigths
self.edgeIntegerWeightsVar = IntVar()
self.graphMenu.add_checkbutton(label='Integer Edge Weights',
command=self.edgeIntegerWeights,
var = self.edgeIntegerWeightsVar)
self.edgeWeightsSubmenu = Menu(self.graphMenu, tearoff=0)
self.edgeWeightVar = IntVar()
self.edgeWeightsSubmenu.add_radiobutton(label="One",
command=self.ChangeEdgeWeights,
var = self.edgeWeightVar, value=1)
self.edgeWeightsSubmenu.add_radiobutton(label="Two",
command=self.ChangeEdgeWeights,
var = self.edgeWeightVar, value=2)
self.edgeWeightsSubmenu.add_radiobutton(label="Three",
command=self.ChangeEdgeWeights,
var = self.edgeWeightVar, value=3)
self.graphMenu.add_cascade(label='Edge Weights',
menu=self.edgeWeightsSubmenu)
self.graphMenu.add_separator()
self.graphMenu.add_checkbutton(label='Grid',
command=self.ToggleGridding)
self.menubar.add_cascade(label="Graph", menu=self.graphMenu,
underline=0)
# Add Tools menu
# self.toolsMenu = Menu(self.menubar,tearoff=1)
# self.toolVar = StringVar()
# self.toolsMenu.add_radiobutton(label='Add or move vertex',
# command=self.ChangeTool,
# var = self.toolVar, value='AddOrMoveVertex')
# self.toolsMenu.add_radiobutton(label='Add edge',
# command=self.ChangeTool,
# var = self.toolVar, value='AddEdge')
# self.toolsMenu.add_radiobutton(label='Delete edge or vertex',
# command=self.ChangeTool,
# var = self.toolVar, value='DeleteEdgeOrVertex')
# self.toolsMenu.add_radiobutton(label='Swap orientation',
# command=self.ChangeTool,
# var = self.toolVar, value='SwapOrientation')
# self.toolsMenu.add_radiobutton(label='Edit Weight',
# command=self.ChangeTool,
# var = self.toolVar, value='EditWeight')
# self.menubar.add_cascade(label="Tools", menu=self.toolsMenu,
# underline=0)
if toplevel:
self.configure(menu=self.menubar)
else:
self.master.configure(menu=self.menubar)
# Add extras menu
self.extrasMenu = Menu(self.menubar, tearoff=0)
# --------------------------------------------------------------
# Add a menue item for all creators found in GraphCreator.creator
for create in GraphCreator.creator:
self.extrasMenu.add_command(label=create.Name(),
command=lambda e=create,s=self:e.Create(s))
# --------------------------------------------------------------
# --------------------------------------------------------------
# Add a menue item for all embedders found in Embedder.embedder
self.extrasMenu.add_separator()
for embed in Embedder.embedder:
self.extrasMenu.add_command(label=embed.Name(),
command=lambda e=embed,s=self:e.Embed(s))
# --------------------------------------------------------------
# --------------------------------------------------------------
self.extrasMenu.add_separator()
self.extrasMenu.add_command(label='Randomize Edge Weights',
command=self.RandomizeEdgeWeights)
self.menubar.add_cascade(label="Extras", menu=self.extrasMenu,
underline=0)
# --------------------------------------------------------------
# On a Mac we put our about box under the Apple menu ...
if os.name == 'mac':
self.apple=Menu(self.menubar, tearoff=0, name='apple')
self.apple.add_command(label='About Gred',
command=self.AboutBox)
self.menubar.add_cascade(menu=self.apple)
else: # ... on other systems we add a help menu
self.helpMenu=Menu(self.menubar, tearoff=0, name='help')
self.helpMenu.add_command(label='About Gred',
command=self.AboutBox)
self.menubar.add_cascade(label="Help", menu=self.helpMenu,
underline=0)
############################################################
#
# Menu Commands
#
# The menu commands are passed as call back parameters to
# the menu items.
#
def NewGraph(self, Directed=1, Euclidean=1, IntegerVertexWeights=0, VertexWeights='None',
IntegerEdgeWeights=0, EdgeWeights='One', Grid=1):
G=None
self.SetGraphMenuDirected(Directed)
self.SetGraphMenuEuclidean(Euclidean)
self.SetGraphMenuIntegerVertexWeights(IntegerVertexWeights)
self.SetGraphMenuVertexWeights(VertexWeights)
self.SetGraphMenuIntegerEdgeWeights(IntegerEdgeWeights)
self.SetGraphMenuEdgeWeights(EdgeWeights)
self.SetGraphMenuGrid(Grid)
self.defaultButton.select()
G = Graph()
G.directed = Directed
G.euclidian = Euclidean
self.graphName = "New"
self.ShowGraph(G,self.graphName)
self.RegisterGraphInformer(WeightedGraphInformer(G,"weight"))
self.fileName = None
self.SetTitle("Gred 0.98J - New Graph")
def OpenGraph(self):
file = askopenfilename(title="Open Graph",
defaultextension=".cat",
filetypes = [ ("Gato", ".cat")
#,("GML", ".gml")
#,("Graphlet", ".let")
]
)
if file is "":
pass
else:
self.fileName = file
self.dirty = 0
self.graphName = stripPath(file)
e = extension(file)
if e == 'cat':
G = OpenCATBoxGraph(file)
elif e == 'gml':
G = OpenGMLGraph(file)
else:
log.error("Unknown extension %s" % e)
if not self.gridding:
self.graphMenu.invoke(self.graphMenu.index('Grid'))
if G.QDirected() != self.directedVar.get():
self.graphMenu.invoke(self.graphMenu.index('Directed'))
if G.QEuclidian() != self.euclideanVar.get():
self.graphMenu.invoke(self.graphMenu.index('Euclidean'))
if G.edgeWeights[0].QInteger() != self.edgeIntegerWeightsVar.get():
self.graphMenu.invoke(self.graphMenu.index('Integer Edge Weights'))
self.graphMenu.invoke(self.graphMenu.index('Integer Vertex Weights'))
# Just one integer flag for vertex and edge weights
if G.NrOfEdgeWeights() == 1:
self.edgeWeightsSubmenu.invoke(self.edgeWeightsSubmenu.index('One'))
elif G.NrOfEdgeWeights() == 2:
self.edgeWeightsSubmenu.invoke(self.edgeWeightsSubmenu.index('Two'))
elif G.NrOfEdgeWeights() == 3:
self.edgeWeightsSubmenu.invoke(self.edgeWeightsSubmenu.index('Three'))
if G.NrOfVertexWeights() == 0 or (G.NrOfVertexWeights() > 0 and
G.vertexWeights[0].QInteger()):
self.graphMenu.invoke(self.graphMenu.index('Integer Vertex Weights'))
if G.NrOfVertexWeights() == 0:
self.vertexWeightsSubmenu.invoke(self.vertexWeightsSubmenu.index('None'))
elif G.NrOfVertexWeights() == 1:
self.vertexWeightsSubmenu.invoke(self.vertexWeightsSubmenu.index('One'))
elif G.NrOfVertexWeights() == 2:
self.vertexWeightsSubmenu.invoke(self.vertexWeightsSubmenu.index('Two'))
elif G.NrOfVertexWeights() == 3:
self.vertexWeightsSubmenu.invoke(self.vertexWeightsSubmenu.index('Three'))
self.RegisterGraphInformer(WeightedGraphInformer(G,"weight"))
self.ShowGraph(G,self.graphName)
self.SetTitle("Gred 0.98J - " + self.graphName)
def SaveGraph(self):
#self.dirty = 0
if self.fileName != None:
SaveCATBoxGraph(self.G,self.fileName)
else:
self.SaveAsGraph()
def SaveAsGraph(self):
file = asksaveasfilename(title="Save Graph",
defaultextension=".cat",
filetypes = [ ("Gato", ".cat")
#,("Graphlet", ".let")
]
)
if file is not "":
self.fileName = file
self.dirty = 0
SaveCATBoxGraph(self.G,file)
self.graphName = stripPath(file)
self.SetTitle("Gred 0.98J - " + self.graphName)
def ExportEPSF(self):
file = asksaveasfilename(title="Export EPSF",
defaultextension=".eps",
filetypes = [ ("Encapsulated PS", ".eps")
,("Postscript", ".ps")
]
)
if file is not "":
self.PrintToPSFile(file)
def Quit(self):
if askokcancel("Quit","Do you really want to quit?"):
Frame.quit(self)
#----- Graph Menu callbacks
def graphDirected(self):
if self.G != None:
if self.G.QDirected():
self.G.Undirect()
else:
self.G.directed = 1
self.ShowGraph(self.G,self.graphName)
def graphEuclidean(self):
if self.G != None:
if self.G.QEuclidian():
self.G.euclidian = 0
else:
self.G.Euclidify()
def edgeIntegerWeights(self):
if self.G != None:
if not self.G.edgeWeights[0].QInteger():
self.G.Integerize('all')
def vertexIntegerWeights(self):
if self.G != None:
for i in xrange(0,self.G.NrOfVertexWeights()):
if not self.G.vertexWeights[i].QInteger():
self.G.vertexWeights[i].Integerize()
else:
self.G.vertexWeights[i] = 0
def ChangeEdgeWeights(self):
if self.G == None:
return
n = self.edgeWeightVar.get()
k = self.G.edgeWeights.keys()
if self.G.edgeWeights[0].QInteger():
initialWeight = 0
else:
initialWeight = 0.0
if n == 1 or n == 2:
if 2 in k:
del(self.G.edgeWeights[2])
else:
if 2 not in k:
self.G.edgeWeights[2] = EdgeWeight(self.G, initialWeight)
if self.G.edgeWeights[0].QInteger():
self.G.edgeWeights[2].Integerize()
if n == 1:
if 1 in k:
del(self.G.edgeWeights[1])
else:
if 1 not in k:
self.G.edgeWeights[1] = EdgeWeight(self.G, initialWeight)
if self.G.edgeWeights[0].QInteger():
self.G.edgeWeights[1].Integerize()
def ChangeVertexWeights(self):
if self.G == None:
return
n = self.vertexWeightVar.get()
old = self.G.NrOfVertexWeights()
k = self.G.vertexWeights.keys()
if self.vertexIntegerWeightsVar.get() == 1:
initialWeight = 0
else:
initialWeight = 0.0
if n > old: # Add additional weigths
for i in xrange(old,n):
self.G.vertexWeights[i] = VertexWeight(self.G, initialWeight)
if self.vertexIntegerWeightsVar.get() == 1:
self.G.vertexWeights[i].Integerize()
else: # Delete superfluos weigths
for i in xrange(n,old):
del(self.G.vertexWeights[i])
# Integerize remaining weigths if necessary
if self.vertexIntegerWeightsVar.get() == 1:
for i in xrange(0,min(n,old)):
self.G.vertexWeights[i].Integerize()
#----- Tools Menu callbacks
def ChangeTool(self):
tool = self.toolVar.get()
if self.lastTool is not None:
self.buttons[self.lastTool].configure(image=self.icons[self.lastTool][0])
self.SetEditMode(tool)
self.lastTool = tool
self.buttons[tool].configure(image=self.icons[tool][2])
#----- Extras Menu callbacks
# NOTE: Embedder handled by lambda passed as command
def RandomizeEdgeWeights(self):
count = len(self.G.edgeWeights.keys())
d = RandomizeEdgeWeightsDialog(self, count, self.G.QEuclidian())
if d.result is None:
return
for e in self.G.Edges():
for i in xrange(count):
if d.result[i][0] == 1:
val = whrandom.uniform(d.result[i][1],d.result[i][2])
if self.G.edgeWeights[i].QInteger():
self.G.edgeWeights[i][e] = round(int(val))
else:
self.G.edgeWeights[i][e] = val
def AboutBox(self):
d = GredAboutBox(self.master)
class SAGraphEditorToplevel(SAGraphEditor, Toplevel):
def __init__(self, master=None):
Toplevel.__init__(self, master)
Splash = GredSplashScreen(self.master)
self.G = None
self.mode = 'AddOrMoveVertex'
self.gridding = 0
self.graphInformer = None
self.makeMenuBar(1)
GraphEditor.__init__(self)
self.fileName = None
self.dirty = 0
self.SetGraphMenuOptions()
Splash.Destroy()
# Fix focus and stacking
self.tkraise()
self.focus_force()
def ExportEPSF(self):
file = asksaveasfilename(title="Export EPSF",
defaultextension=".eps",
filetypes = [ ("Encapsulated PS", ".eps")
,("Postscript", ".ps")
]
)
if file is not "":
self.PrintToPSFile(file)
self.tkraise()
self.focus_force()
def AboutBox(self):
d = GredAboutBox(self)
def SetTitle(self,title):
self.title(title)
self.tkraise()
self.focus_force()
def Quit(self):
if askokcancel("Quit","Do you really want to quit?"):
self.destroy()
else:
self.tkraise()
self.focus_force()
class Start:
def __init__(self):
graphEditor = SAGraphEditorToplevel()
graphEditor.NewGraph()
import logging
log = logging.getLogger("Gred.py")
################################################################################
if __name__ == '__main__':
## globals()['gVertexRadius'] = 12
## globals()['gVertexFrameWidth'] = 0
## globals()['gEdgeWidth'] = 2
GatoGlobals.cVertexDefault = '#000099'
## globals()['cEdgeDefault'] = '#999999'
## globals()['cLabelDefault'] = 'white'
# Overide default colors for widgets ... maybe shouldnt be doing that for Windows?
tk = Tk()
tk.option_add('*ActiveBackground','#EEEEEE')
tk.option_add('*background','#DDDDDD')
tk.option_add('Tk*Scrollbar.troughColor','#CACACA')
graphEditor = SAGraphEditor(tk)
graphEditor.NewGraph()
import logging
log = logging.getLogger("Gred.py")
graphEditor.mainloop()
------------------------------------------------
# --------------------------------------------------------------
self.extrasMenu.add_separator()
self.extrasMenu.add_command(label='Randomize Edge Weights',
Gato/LGPL.txt 0100644 0017676 0017534 00000061273 10014153354 0014324 0 ustar 00schliep catbox 0000305 0050066 GNU LIBRARY GENERAL PUBLIC LICENSE
Version 2, June 1991
Copyright (C) 1991 Free Software Foundation, Inc.
59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
[This is the first released version of the library GPL. It is
numbered 2 because it goes with version 2 of the ordinary GPL.]
Preamble
The licenses for most software are designed to take away your
freedom to share and change it. By contrast, the GNU General Public
Licenses are intended to guarantee your freedom to share and change
free software--to make sure the software is free for all its users.
This license, the Library General Public License, applies to some
specially designated Free Software Foundation software, and to any
other libraries whose authors decide to use it. You can use it for
your libraries, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
this service if you wish), that you receive source code or can get it
if you want it, that you can change the software or use pieces of it
in new free programs; and that you know you can do these things.
To protect your rights, we need to make restrictions that forbid
anyone to deny you these rights or to ask you to surrender the rights.
These restrictions translate to certain responsibilities for you if
you distribute copies of the library, or if you modify it.
For example, if you distribute copies of the library, whether gratis
or for a fee, you must give the recipients all the rights that we gave
you. You must make sure that they, too, receive or can get the source
code. If you link a program with the library, you must provide
complete object files to the recipients so that they can relink them
with the library, after making changes to the library and recompiling
it. And you must show them these terms so they know their rights.
Our method of protecting your rights has two steps: (1) copyright
the library, and (2) offer you this license which gives you legal
permission to copy, distribute and/or modify the library.
Also, for each distributor's protection, we want to make certain
that everyone understands that there is no warranty for this free
library. If the library is modified by someone else and passed on, we
want its recipients to know that what they have is not the original
version, so that any problems introduced by others will not reflect on
the original authors' reputations.
Finally, any free program is threatened constantly by software
patents. We wish to avoid the danger that companies distributing free
software will individually obtain patent licenses, thus in effect
transforming the program into proprietary software. To prevent this,
we have made it clear that any patent must be licensed for everyone's
free use or not licensed at all.
Most GNU software, including some libraries, is covered by the ordinary
GNU General Public License, which was designed for utility programs. This
license, the GNU Library General Public License, applies to certain
designated libraries. This license is quite different from the ordinary
one; be sure to read it in full, and don't assume that anything in it is
the same as in the ordinary license.
The reason we have a separate public license for some libraries is that
they blur the distinction we usually make between modifying or adding to a
program and simply using it. Linking a program with a library, without
changing the library, is in some sense simply using the library, and is
analogous to running a utility program or application program. However, in
a textual and legal sense, the linked executable is a combined work, a
derivative of the original library, and the ordinary General Public License
treats it as such.
Because of this blurred distinction, using the ordinary General
Public License for libraries did not effectively promote software
sharing, because most developers did not use the libraries. We
concluded that weaker conditions might promote sharing better.
However, unrestricted linking of non-free programs would deprive the
users of those programs of all benefit from the free status of the
libraries themselves. This Library General Public License is intended to
permit developers of non-free programs to use free libraries, while
preserving your freedom as a user of such programs to change the free
libraries that are incorporated in them. (We have not seen how to achieve
this as regards changes in header files, but we have achieved it as regards
changes in the actual functions of the Library.) The hope is that this
will lead to faster development of free libraries.
The precise terms and conditions for copying, distribution and
modification follow. Pay close attention to the difference between a
"work based on the library" and a "work that uses the library". The
former contains code derived from the library, while the latter only
works together with the library.
Note that it is possible for a library to be covered by the ordinary
General Public License rather than by this special one.
GNU LIBRARY GENERAL PUBLIC LICENSE
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
0. This License Agreement applies to any software library which
contains a notice placed by the copyright holder or other authorized
party saying it may be distributed under the terms of this Library
General Public License (also called "this License"). Each licensee is
addressed as "you".
A "library" means a collection of software functions and/or data
prepared so as to be conveniently linked with application programs
(which use some of those functions and data) to form executables.
The "Library", below, refers to any such software library or work
which has been distributed under these terms. A "work based on the
Library" means either the Library or any derivative work under
copyright law: that is to say, a work containing the Library or a
portion of it, either verbatim or with modifications and/or translated
straightforwardly into another language. (Hereinafter, translation is
included without limitation in the term "modification".)
"Source code" for a work means the preferred form of the work for
making modifications to it. For a library, complete source code means
all the source code for all modules it contains, plus any associated
interface definition files, plus the scripts used to control compilation
and installation of the library.
Activities other than copying, distribution and modification are not
covered by this License; they are outside its scope. The act of
running a program using the Library is not restricted, and output from
such a program is covered only if its contents constitute a work based
on the Library (independent of the use of the Library in a tool for
writing it). Whether that is true depends on what the Library does
and what the program that uses the Library does.
1. You may copy and distribute verbatim copies of the Library's
complete source code as you receive it, in any medium, provided that
you conspicuously and appropriately publish on each copy an
appropriate copyright notice and disclaimer of warranty; keep intact
all the notices that refer to this License and to the absence of any
warranty; and distribute a copy of this License along with the
Library.
You may charge a fee for the physical act of transferring a copy,
and you may at your option offer warranty protection in exchange for a
fee.
2. You may modify your copy or copies of the Library or any portion
of it, thus forming a work based on the Library, and copy and
distribute such modifications or work under the terms of Section 1
above, provided that you also meet all of these conditions:
a) The modified work must itself be a software library.
b) You must cause the files modified to carry prominent notices
stating that you changed the files and the date of any change.
c) You must cause the whole of the work to be licensed at no
charge to all third parties under the terms of this License.
d) If a facility in the modified Library refers to a function or a
table of data to be supplied by an application program that uses
the facility, other than as an argument passed when the facility
is invoked, then you must make a good faith effort to ensure that,
in the event an application does not supply such function or
table, the facility still operates, and performs whatever part of
its purpose remains meaningful.
(For example, a function in a library to compute square roots has
a purpose that is entirely well-defined independent of the
application. Therefore, Subsection 2d requires that any
application-supplied function or table used by this function must
be optional: if the application does not supply it, the square
root function must still compute square roots.)
These requirements apply to the modified work as a whole. If
identifiable sections of that work are not derived from the Library,
and can be reasonably considered independent and separate works in
themselves, then this License, and its terms, do not apply to those
sections when you distribute them as separate works. But when you
distribute the same sections as part of a whole which is a work based
on the Library, the distribution of the whole must be on the terms of
this License, whose permissions for other licensees extend to the
entire whole, and thus to each and every part regardless of who wrote
it.
Thus, it is not the intent of this section to claim rights or contest
your rights to work written entirely by you; rather, the intent is to
exercise the right to control the distribution of derivative or
collective works based on the Library.
In addition, mere aggregation of another work not based on the Library
with the Library (or with a work based on the Library) on a volume of
a storage or distribution medium does not bring the other work under
the scope of this License.
3. You may opt to apply the terms of the ordinary GNU General Public
License instead of this License to a given copy of the Library. To do
this, you must alter all the notices that refer to this License, so
that they refer to the ordinary GNU General Public License, version 2,
instead of to this License. (If a newer version than version 2 of the
ordinary GNU General Public License has appeared, then you can specify
that version instead if you wish.) Do not make any other change in
these notices.
Once this change is made in a given copy, it is irreversible for
that copy, so the ordinary GNU General Public License applies to all
subsequent copies and derivative works made from that copy.
This option is useful when you wish to copy part of the code of
the Library into a program that is not a library.
4. You may copy and distribute the Library (or a portion or
derivative of it, under Section 2) in object code or executable form
under the terms of Sections 1 and 2 above provided that you accompany
it with the complete corresponding machine-readable source code, which
must be distributed under the terms of Sections 1 and 2 above on a
medium customarily used for software interchange.
If distribution of object code is made by offering access to copy
from a designated place, then offering equivalent access to copy the
source code from the same place satisfies the requirement to
distribute the source code, even though third parties are not
compelled to copy the source along with the object code.
5. A program that contains no derivative of any portion of the
Library, but is designed to work with the Library by being compiled or
linked with it, is called a "work that uses the Library". Such a
work, in isolation, is not a derivative work of the Library, and
therefore falls outside the scope of this License.
However, linking a "work that uses the Library" with the Library
creates an executable that is a derivative of the Library (because it
contains portions of the Library), rather than a "work that uses the
library". The executable is therefore covered by this License.
Section 6 states terms for distribution of such executables.
When a "work that uses the Library" uses material from a header file
that is part of the Library, the object code for the work may be a
derivative work of the Library even though the source code is not.
Whether this is true is especially significant if the work can be
linked without the Library, or if the work is itself a library. The
threshold for this to be true is not precisely defined by law.
If such an object file uses only numerical parameters, data
structure layouts and accessors, and small macros and small inline
functions (ten lines or less in length), then the use of the object
file is unrestricted, regardless of whether it is legally a derivative
work. (Executables containing this object code plus portions of the
Library will still fall under Section 6.)
Otherwise, if the work is a derivative of the Library, you may
distribute the object code for the work under the terms of Section 6.
Any executables containing that work also fall under Section 6,
whether or not they are linked directly with the Library itself.
6. As an exception to the Sections above, you may also compile or
link a "work that uses the Library" with the Library to produce a
work containing portions of the Library, and distribute that work
under terms of your choice, provided that the terms permit
modification of the work for the customer's own use and reverse
engineering for debugging such modifications.
You must give prominent notice with each copy of the work that the
Library is used in it and that the Library and its use are covered by
this License. You must supply a copy of this License. If the work
during execution displays copyright notices, you must include the
copyright notice for the Library among them, as well as a reference
directing the user to the copy of this License. Also, you must do one
of these things:
a) Accompany the work with the complete corresponding
machine-readable source code for the Library including whatever
changes were used in the work (which must be distributed under
Sections 1 and 2 above); and, if the work is an executable linked
with the Library, with the complete machine-readable "work that
uses the Library", as object code and/or source code, so that the
user can modify the Library and then relink to produce a modified
executable containing the modified Library. (It is understood
that the user who changes the contents of definitions files in the
Library will not necessarily be able to recompile the application
to use the modified definitions.)
b) Accompany the work with a written offer, valid for at
least three years, to give the same user the materials
specified in Subsection 6a, above, for a charge no more
than the cost of performing this distribution.
c) If distribution of the work is made by offering access to copy
from a designated place, offer equivalent access to copy the above
specified materials from the same place.
d) Verify that the user has already received a copy of these
materials or that you have already sent this user a copy.
For an executable, the required form of the "work that uses the
Library" must include any data and utility programs needed for
reproducing the executable from it. However, as a special exception,
the source code distributed need not include anything that is normally
distributed (in either source or binary form) with the major
components (compiler, kernel, and so on) of the operating system on
which the executable runs, unless that component itself accompanies
the executable.
It may happen that this requirement contradicts the license
restrictions of other proprietary libraries that do not normally
accompany the operating system. Such a contradiction means you cannot
use both them and the Library together in an executable that you
distribute.
7. You may place library facilities that are a work based on the
Library side-by-side in a single library together with other library
facilities not covered by this License, and distribute such a combined
library, provided that the separate distribution of the work based on
the Library and of the other library facilities is otherwise
permitted, and provided that you do these two things:
a) Accompany the combined library with a copy of the same work
based on the Library, uncombined with any other library
facilities. This must be distributed under the terms of the
Sections above.
b) Give prominent notice with the combined library of the fact
that part of it is a work based on the Library, and explaining
where to find the accompanying uncombined form of the same work.
8. You may not copy, modify, sublicense, link with, or distribute
the Library except as expressly provided under this License. Any
attempt otherwise to copy, modify, sublicense, link with, or
distribute the Library is void, and will automatically terminate your
rights under this License. However, parties who have received copies,
or rights, from you under this License will not have their licenses
terminated so long as such parties remain in full compliance.
9. You are not required to accept this License, since you have not
signed it. However, nothing else grants you permission to modify or
distribute the Library or its derivative works. These actions are
prohibited by law if you do not accept this License. Therefore, by
modifying or distributing the Library (or any work based on the
Library), you indicate your acceptance of this License to do so, and
all its terms and conditions for copying, distributing or modifying
the Library or works based on it.
10. Each time you redistribute the Library (or any work based on the
Library), the recipient automatically receives a license from the
original licensor to copy, distribute, link with or modify the Library
subject to these terms and conditions. You may not impose any further
restrictions on the recipients' exercise of the rights granted herein.
You are not responsible for enforcing compliance by third parties to
this License.
11. If, as a consequence of a court judgment or allegation of patent
infringement or for any other reason (not limited to patent issues),
conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot
distribute so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you
may not distribute the Library at all. For example, if a patent
license would not permit royalty-free redistribution of the Library by
all those who receive copies directly or indirectly through you, then
the only way you could satisfy both it and this License would be to
refrain entirely from distribution of the Library.
If any portion of this section is held invalid or unenforceable under any
particular circumstance, the balance of the section is intended to apply,
and the section as a whole is intended to apply in other circumstances.
It is not the purpose of this section to induce you to infringe any
patents or other property right claims or to contest validity of any
such claims; this section has the sole purpose of protecting the
integrity of the free software distribution system which is
implemented by public license practices. Many people have made
generous contributions to the wide range of software distributed
through that system in reliance on consistent application of that
system; it is up to the author/donor to decide if he or she is willing
to distribute software through any other system and a licensee cannot
impose that choice.
This section is intended to make thoroughly clear what is believed to
be a consequence of the rest of this License.
12. If the distribution and/or use of the Library is restricted in
certain countries either by patents or by copyrighted interfaces, the
original copyright holder who places the Library under this License may add
an explicit geographical distribution limitation excluding those countries,
so that distribution is permitted only in or among countries not thus
excluded. In such case, this License incorporates the limitation as if
written in the body of this License.
13. The Free Software Foundation may publish revised and/or new
versions of the Library General Public License from time to time.
Such new versions will be similar in spirit to the present version,
but may differ in detail to address new problems or concerns.
Each version is given a distinguishing version number. If the Library
specifies a version number of this License which applies to it and
"any later version", you have the option of following the terms and
conditions either of that version or of any later version published by
the Free Software Foundation. If the Library does not specify a
license version number, you may choose any version ever published by
the Free Software Foundation.
14. If you wish to incorporate parts of the Library into other free
programs whose distribution conditions are incompatible with these,
write to the author to ask for permission. For software which is
copyrighted by the Free Software Foundation, write to the Free
Software Foundation; we sometimes make exceptions for this. Our
decision will be guided by the two goals of preserving the free status
of all derivatives of our free software and of promoting the sharing
and reuse of software generally.
NO WARRANTY
15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO
WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW.
EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR
OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY
KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE
LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME
THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN
WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY
AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU
FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR
CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE
LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING
RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A
FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF
SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
DAMAGES.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Libraries
If you develop a new library, and you want it to be of the greatest
possible use to the public, we recommend making it free software that
everyone can redistribute and change. You can do so by permitting
redistribution under these terms (or, alternatively, under the terms of the
ordinary General Public License).
To apply these terms, attach the following notices to the library. It is
safest to attach them to the start of each source file to most effectively
convey the exclusion of warranty; and each file should have at least the
"copyright" line and a pointer to where the full notice is found.
Copyright (C)
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Library General Public
License as published by the Free Software Foundation; either
version 2 of the License, or (at your option) any later version.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Library General Public License for more details.
You should have received a copy of the GNU Library General Public
License along with this library; if not, write to the Free
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
Also add information on how to contact you by electronic and paper mail.
You should also get your employer (if you work as a programmer) or your
school, if any, to sign a "copyright disclaimer" for the library, if
necessary. Here is a sample; alter the names:
Yoyodyne, Inc., hereby disclaims all copyright interest in the
library `Frob' (a library for tweaking knobs) written by James Random Hacker.
, 1 April 1990
Ty Coon, President of Vice
That's all there is to it!
r, valid for at
least three years, to give the same user the materials
specified in Subsection 6a, above, for a charge no more
than the cost of performing this distribution.
c) If distribution of the work is made by offering access to copy
from a designated place, offer equivalent access to copy the aboGato/PlanarEmbedding.py 0100644 0017676 0017534 00000107207 10014153354 0016411 0 ustar 00schliep catbox 0000305 0050066 ################################################################################
#
# This file is part of Gato (Graph Algorithm Toolbox)
#
# file: PlanarEmbedding.py
# author: Ramazan Buzdemir (buzdemir@zpr.uni-koeln.de)
#
# Copyright (C) 1998-2003, Alexander Schliep, Winfried Hochstaettler and
# ZAIK/ZPR, Universitaet zu Koeln
#
# Contact: schliep@molgen.mpg.de, wh@zpr.uni-koeln.de
#
# Information: http://gato.sf.net
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Library General Public
# License as published by the Free Software Foundation; either
# version 2 of the License, or (at your option) any later version.
#
# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Library General Public License for more details.
#
# You should have received a copy of the GNU Library General Public
# License along with this library; if not, write to the Free
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#
#
#
# This file is version $Revision: 1.11 $
# from $Date: 2003/02/12 14:24:59 $
# last change by $Author: schliep $.
#
################################################################################
###############################################################################
###############################################################################
###############################################################################
# #
# AN IMPLEMENTATION OF #
# THE "DE FRAYSSEIX,PACH,POLLACK(FPP)" #
# AND THE "SCHNYDER" #
# PLANAR STRAIGHT-LINE EMBEDDING ALGORITHM #
# #
###############################################################################
# #
# References: #
# #
# [FPP90] H. de Fraysseix, J.Pach, and R.Pollack . #
# "How to draw a planar graph on a grid." #
# Combinatorian, 10:41-51,1990 #
# [Sch90] W.Schnyder. #
# "Embedding planar graphs on the grid." #
# In 1st Annual ACM-SIAM Symposium on Discrete Algorithms, #
# pages 138-14, San Francisco, 1990 #
# #
###############################################################################
#=============================================================================#
from PlanarityTest import *
from copy import deepcopy
from DataStructures import Stack
from tkMessageBox import showinfo
#=============================================================================#
#=============================================================================#
class pe_Point:
def __init__(self,xpos,ypos):
self.x=xpos
self.y=ypos
#=============================================================================#
#=============================================================================#
class pe_Node:
def __init__(self,x,y):
self.xpos=x
self.ypos=y
self.canOrder=None
self.t1,self.t2,self.t3=None,None,None
self.p1,self.p2,self.p3=None,None,None
self.r1,self.r2,self.r3=None,None,None
self.xsch,self.ysch=None,None
self.xfpp,self.yfpp=None,None
self.adjacentEdges=[]
self.adjacentNodes=[]
self.M=[]
self.oppositeNodes=[]
self.outface=None
self.path1=[]
self.path2=[]
self.path3=[]
def addEdge(self,e,v):
self.adjacentEdges.append(e)
self.adjacentNodes.append(v)
#=============================================================================#
#=============================================================================#
class pe_Edge: # directed from p1->p2
def __init__(self,index_p1,index_p2,ep1,ep2,tf):
self.p1=index_p1
self.p2=index_p2
self.label=None # normal labelling: 1,-1,2,-2,3,-3
self.original=tf
self.outface=None
#=============================================================================#
#=============================================================================#
class pe_Graph:
#-------------------------------------------------------------------------
def printGraph(self):
for i in range(0,len(self.nodes)):
n=self.nodes[i]
print "--------Node:",i,"--------------"
print "xpos=",n.xpos,"ypos=",n.ypos
print "canOrder=",n.canOrder
print "t1=",n.t1,"t2=",n.t2,"t3=",n.t3
print "p1=",n.p1,"p2=",n.p2,"p3=",n.p3
print "r1=",n.r1,"r2=",n.r2,"r3=",n.r3
print "xsch=",n.xsch,"ysch=",n.ysch
print "xfpp=",n.xfpp,"yfpp=",n.yfpp
print "outface=",n.outface
print
for i in range(0,len(self.edges)):
e=self.edges[i]
print "-------Edge:",i,"---------------"
print "p1=",e.p1,"p2=",e.p2
print "label=",e.label
print "original=",e.original,"outface=",e.outface
print
for i in range(0,len(self.nodes)):
n=self.nodes[i]
print "---------------------------"
print i,":"
print "adjacentEdges:",
for j in range(0,len(n.adjacentEdges)):
print n.adjacentEdges[j],
print
print "adjacentNodes:",
for j in range(0,len(n.adjacentNodes)):
print n.adjacentNodes[j],
print
print "M:",
for j in range(0,len(n.M)):
print n.M[j],
print
print "oppositeNodes:",
for j in range(0,len(n.oppositeNodes)):
print n.oppositeNodes[j],
print
print "path1:",
for j in range(0,len(n.path1)):
print n.path1[j],
print
print "path2:",
for j in range(0,len(n.path2)):
print n.path2[j],
print
print "path3:",
for j in range(0,len(n.path3)):
print n.path3[j],
print
#-------------------------------------------------------------------------
#-------------------------------------------------------------------------
def __init__(self):
self.nodes=[]
self.edges=[]
self.orderK,self.orderIndexVk=None,None
self.FPPk=None
self.labelK=None
self.indexV1,self.indexV2,self.indexV3=-1,-1,-1
#-------------------------------------------------------------------------
#-------------------------------------------------------------------------
def checkIndex(self,index, p1):
if index<0:
tempNode1=pe_Node(p1.x,p1.y)
self.nodes.append(tempNode1)
return (len(self.nodes)-1)
return index
#-------------------------------------------------------------------------
#-------------------------------------------------------------------------
def storeEdge(self,indexP1,indexP2,p1,p2,tf):
ep1=pe_Point(self.nodes[indexP1].xpos,self.nodes[indexP1].ypos)
ep2=pe_Point(self.nodes[indexP2].xpos,self.nodes[indexP2].ypos)
self.edges.append(pe_Edge(indexP1,indexP2,ep1,ep2,tf))
#-------------------------------------------------------------------------
#+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
# TRIANGULATION
#+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
# Algorithm:
# For each vertex v
# for v's each pair of consecutive neighbours u & w
# add the edge in
# add u into w's incident list in ccw order
# add w into u's incident list in ccw order
# repeat this procedure
#+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
#-------------------------------------------------------------------------
def isEdge(self,u,w):
# check if w is in u's adjacentEdges
if w in u.adjacentNodes: return 1
return 0
#-------------------------------------------------------------------------
#-------------------------------------------------------------------------
def adjacentVertex(self,v,e):
return v.adjacentNodes[e]
#-------------------------------------------------------------------------
#-------------------------------------------------------------------------
def consider(self):
for indexV in range(0,len(self.nodes)):
v=self.nodes[indexV]
if len(v.adjacentEdges)<2: continue
for j in range(0,len(v.adjacentEdges)):
# get two consective neighbours of v
indexU=self.adjacentVertex(v,j)
u=self.nodes[indexU]
k=j+1
if k==len(v.adjacentEdges): k=0
indexW=self.adjacentVertex(v,k)
w=self.nodes[indexW]
# check if (u, w) is an edge
if not(self.isEdge(u,indexW)):
pointu=pe_Point(u.xpos,u.ypos)
pointw=pe_Point(w.xpos,w.ypos)
self.storeEdge(indexU,indexW,pointu, pointw,0)
tempi1=indexV
tempe1=len(self.edges)-1
# add u to w's adjacentEdges (with ordering)
# add u after v in w's adjacentEdges
indexVinW=w.adjacentNodes.index(tempi1)+1
w.adjacentEdges.insert(indexVinW,tempe1)
w.adjacentNodes.insert(indexVinW,indexU)
# add w to u's incitentList (with ordering)
# add w before v in u's adjacentEdges
indexVinU=u.adjacentNodes.index(tempi1)
u.adjacentEdges.insert(indexVinU,tempe1)
u.adjacentNodes.insert(indexVinU,indexW)
# Don't forget to set original=0
self.edges[-1].original=0
return 1
return 0
#-------------------------------------------------------------------------
#-------------------------------------------------------------------------
def triangulate(self):
finish=1
while finish:
finish=self.consider()
#-------------------------------------------------------------------------
#+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
#+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
# CANONICAL ORDERING
#+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
# Algorithm:
# Pick up a face as outface
# Assign its vertices with canonical ordering 1,2, and n
#
# For k from n-1 to 3
# remove Vk+1 from graph
# find all Vk+1's neighbours in the new graph Gk
# update the vertices on the outface
# assign Vk to one of these neighbours on Ck that
# is not V1
# is not V2
# is not incident to a chord
#+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
#-------------------------------------------------------------------------
def ordering(self):
self.orderK=len(self.nodes)
# Now, remove Vn from the graph, and let Vn-1 be the vertex
# that is on the outerface and not incident to a chord.
k=len(self.nodes)
while k>3:
self.orderIndexVk=self.order()
k=k-1
#-------------------------------------------------------------------------
#-------------------------------------------------------------------------
def initOrder(self):
# NOTE: initially, all the canOrder are 0
for i in range(0,len(self.nodes)):
self.nodes[i].canOrder=0
# Base: find v1, v2, and vn, which define a outerface
if self.indexV1<0:
self.indexV1=0
v1=self.nodes[self.indexV1]
self.indexV2=v1.adjacentNodes[0]
v2=self.nodes[self.indexV2]
self.indexVn=v1.adjacentNodes[1]
vk=self.nodes[self.indexVn]
else:
v1=self.nodes[self.indexV1]
v2=self.nodes[self.indexV2]
vk=self.nodes[self.indexVn]
v1.canOrder=1
v2.canOrder=2
vk.canOrder=len(self.nodes)
# initialize all the outface to 0
for j in range(0,len(self.nodes)):
self.nodes[j].outface=0
self.orderK=len(self.nodes)
self.orderIndexVk=self.indexVn
return self.indexVn
#-------------------------------------------------------------------------
#-------------------------------------------------------------------------
# Now, remove Vn from the graph, and let Vn-1 be the vertex
# that is on the outerface and not incident to a chord
def order(self):
if self.orderK>3:
v1=self.nodes[self.indexV1]
v2=self.nodes[self.indexV2]
vk=self.nodes[self.orderIndexVk]
# "remove" Vk from the graph
# find the neighbours of Vk, that have canOrder number < k
# define they are on the outface
for i in range(0,len(vk.adjacentNodes)):
neighbour=self.nodes[vk.adjacentNodes[i]]
if neighbour.canOrderV1 with 1
# label V3->V2 with 2
#
# For k from 3 to n-1
# add Vk+1 to graph Gk
# find all Vk+1's neighbours in Gk in order
# label the left most edge from top to bottom with 1
# label the right most edge from top to bottom with 2
# label the rest from bottom to top with 3
#+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
#-------------------------------------------------------------------------
def labelling(self):
self.initLabel()
#steps
for k in range(3,len(self.nodes)):
self.labelK=k
self.labelStep()
# looking for v4, v5, ..., vn
#-------------------------------------------------------------------------
#-------------------------------------------------------------------------
# label label if the edge is indexP1 -> indexP2
# -label if the edge is indexP2 -> indexP1
def labelEdge(self,indexP1,indexP2,label):
e=self.edges[0]
if e.p1==indexP1 and e.p2==indexP2:
self.edges[0].label=label
return
if e.p1==indexP2 and e.p2==indexP1:
self.edges[0].label=-label
return
for i in range(1,len(self.edges)):
e=self.edges[i]
if e.p1==indexP1 and e.p2==indexP2:
e.label=label
return
if e.p1==indexP2 and e.p2==indexP1:
e.label=-label
return
#-------------------------------------------------------------------------
#-------------------------------------------------------------------------
def findIndexOfVk(self,k):
indexVk=-1
i=0
while indexVk<0:
if self.nodes[i].canOrder==k:
return i
i=i+1
return -1
#-------------------------------------------------------------------------
#-------------------------------------------------------------------------
def initLabel(self):
for j in range(0,len(self.edges)):
self.edges[j].label=0
self.indexV3=self.findIndexOfVk(3)
# find v1, v2, v3
v1=self.nodes[self.indexV1]
v2=self.nodes[self.indexV2]
v3=self.nodes[self.indexV3]
# labelling should be done at the same time as FPP is running
# (because we need the outface information)
# but we are doing this separately, for the sak of clearness
# label V3 -> V1 by 1
self.labelEdge(self.indexV3,self.indexV1,1)
# label V3 -> V2 by 2
self.labelEdge(self.indexV3,self.indexV2,2)
self.labelK = 3
#-------------------------------------------------------------------------
#-------------------------------------------------------------------------
def labelStep(self):
k=self.labelK
n=len(self.nodes)
if k2:
for l in range(1,len(vkplus1.oppositeNodes)-1):
self.labelEdge(indexVkplus1,vkplus1.oppositeNodes[l],-3)
self.labelK=self.labelK+1
return self.labelK
#-------------------------------------------------------------------------
#+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
#+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
# FPP
#+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
# Algorithm:
# 3. Initialize x,y coordinates and M for V1,V2, and V3
# v1.M={v1,v2,v3}
# v2.M={v2}
# v3.M={v2,v3}
# 4. In the canonical order, for each vertex
# 1. find the vertices on the outface in order
# 2. shift vertices in the subset M of Wp+1 and Wq
# 3. calculate the x,y coordinates of Vk+1
# 4. updating M for all the outface vertices
# wi.M=wi.M+{vk+1} for i<=p
# vk+1.M=wp+1.M+{vk+1}
# wj.M=wj.M for j>=q
#+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
#-------------------------------------------------------------------------
def FPP(self):
self.initFPP()
# steps
for k in range(3,len(self.nodes)):
self.FPPk=k
self.FPPstep()
#-------------------------------------------------------------------------
#-------------------------------------------------------------------------
def initFPP(self):
self.indexV3=self.findIndexOfVk(3)
# initialize all the outface to 0
for j in range(0,len(self.nodes)):
self.nodes[j].outface=0
self.nodes[j].xfpp=0
self.nodes[j].yfpp=0
self.nodes[j].M=[]
self.nodes[j].oppositeNodes=[]
# find v1, v2, v3
v1=self.nodes[self.indexV1]
v2=self.nodes[self.indexV2]
v3=self.nodes[self.indexV3]
# basic
v1.xfpp=0; v1.yfpp=0
v2.xfpp=2; v2.yfpp=0
v3.xfpp=1; v3.yfpp=1
# v1.M={v1,v2,v3} v2.M={v2} v3.M={v2,v3}
v1.M.append(self.indexV1)
v1.M.append(self.indexV2)
v1.M.append(self.indexV3)
v2.M.append(self.indexV2)
v3.M.append(self.indexV2)
v3.M.append(self.indexV3)
self.nodes[self.indexV1].outface=1
self.nodes[self.indexV2].outface=1
self.nodes[self.indexV3].outface=1
self.FPPk=3
#-------------------------------------------------------------------------
#-------------------------------------------------------------------------
def FPPstep(self):
k=self.FPPk
n=len(self.nodes)
if k2:
for i in range(1,len(vkplus1.oppositeNodes)-1):
temp=vkplus1.oppositeNodes[i]
self.nodes[temp].outface=0
# shift all vertices in w(p+1).M right by 1 unit
indexWpplus1=vkplus1.oppositeNodes[1]
w=self.nodes[indexWpplus1]
for i in range(0,len(w.M)):
temp=w.M[i]
self.nodes[temp].xfpp=self.nodes[temp].xfpp+1
# shift all vertices in w(q).M right by 1 unit
Wq=self.nodes[vkplus1.oppositeNodes[-1]]
for i in range(0,len(Wq.M)):
self.nodes[Wq.M[i]].xfpp=self.nodes[Wq.M[i]].xfpp+1
# add in v(k+1)
Wp=self.nodes[vkplus1.oppositeNodes[0]]
x1=Wp.xfpp
y1=Wp.yfpp
x2=Wq.xfpp
y2=Wq.yfpp
vkplus1.xfpp=(x1+x2+y2-y1)/2
vkplus1.yfpp=(x2-x1+y2+y1)/2
# update M
# wi.M = wi.M + v(k+1) for i<=p
for i in range(0,n):
wi=self.nodes[i]
if wi.outface and wi.xfpp(v1',v2',v3')/(n-1)
# A barycentric representation of a graph G is
# an injective function v->(v1,v2,v3) that satisfies:
# a.) v1+v2+v3=1 for all v
# b.) for each edge (x,y) and each vertex z not x or y,
# there is some k (k=1,2 or 3) such that xk < zk and yk < zk
#+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
#-------------------------------------------------------------------------
def calculateP(self,path123):
for i in range(0,len(self.nodes)):
v=self.nodes[i]
finalNode=0
if (v.canOrder!=1 and v.canOrder!=2 and
v.canOrder!=len(self.nodes)):
# v is an interior vertex
if path123==1:
v.p1=1
v.path1.append(i)
finalNode=1
if path123==2:
v.p2=1
v.path2.append(i)
finalNode=2
if path123==3:
v.p3=1
v.path3.append(i)
finalNode=len(self.nodes)
vNext=v
while vNext.canOrder!=finalNode:
j=0
found=0
while (not(found) and j=q
#-------------------------------------------------------------------------
# LOAD GRAPH
graph=load_graph(G)
if graph==0: return 0
#-------------------------------------------------------------------------
#-------------------------------------------------------------------------
# 1.TRIANGULATION
graph.triangulate()
#-------------------------------------------------------------------------
#-------------------------------------------------------------------------
# 2.CANONICAL ORDERING
graph.initOrder()
graph.ordering()
#-------------------------------------------------------------------------
#-------------------------------------------------------------------------
# 3+4. FPP
graph.FPP()
#-------------------------------------------------------------------------
#-------------------------------------------------------------------------
# COORDINATES
G.xCoord={}
G.yCoord={}
n=len(graph.nodes)
for i in range(0,n):
G.xCoord[G.vertices[i]]=graph.nodes[i].xfpp*float(900/(2*n-4))+50
G.yCoord[G.vertices[i]]=1000-(graph.nodes[i].yfpp*float(900/(n-2))+50)
return 1
#-------------------------------------------------------------------------
#=============================================================================#
#=============================================================================#
def Schnyder_PlanarCoords(G): # (n-1)*(n-1) GRID
# Algorithm:
# 1. Triangulate orginal graph
# 2. Canonical order all vertices
# 3. Normal label interior edges of G to i->Ti (i=1,2,3)
# 4. Calculate for each interior vertex v
# pathi : vertices on the i-path from v to v1, v2, or vn
# pn : number of vertices on the i-path starting at v
# ti : number of vertices in the subtree of Ti rooted at v
# ri : number of vertices in region Ri(v) for v
# 5. Calculate barycentric representation for each v: vi'=ri - pi-1
# v->(v1',v2',v3')/(n-1)
# A barycentric representation of a graph G is
# an injective function v->(v1,v2,v3) that satisfies:
# a.) v1+v2+v3=1 for all v
# b.) for each edge (x,y) and each vertex z not x or y,
# there is some k (k=1,2 or 3) such that xk < zk and yk < zk
#-------------------------------------------------------------------------
# LOAD GRAPH
graph=load_graph(G)
if graph==0: return 0
#-------------------------------------------------------------------------
#-------------------------------------------------------------------------
# 1.TRIANGULATION
graph.triangulate()
#-------------------------------------------------------------------------
#-------------------------------------------------------------------------
# 2.CANONICAL ORDERING
graph.initOrder()
graph.ordering()
#-------------------------------------------------------------------------
#-------------------------------------------------------------------------
# 3. EDGE LABELLING
graph.FPP() # outfaces
graph.labelling()
#-------------------------------------------------------------------------
#-------------------------------------------------------------------------
# 4+5. Schnyder
graph.Schnyder()
#-------------------------------------------------------------------------
#-------------------------------------------------------------------------
# COORDINATES
G.xCoord={}
G.yCoord={}
n=len(graph.nodes)
for i in range(0,n):
G.xCoord[G.vertices[i]]=graph.nodes[i].xsch*float(900/(n-1))+50
G.yCoord[G.vertices[i]]=1000-(graph.nodes[i].ysch*float(900/(n-1))+50)
return 1
#-------------------------------------------------------------------------
#=============================================================================#
HLER !!!!!!
if ( ((curLabel==path123) and
(curEdge.p1==self.nodes.index(vNext))) or
((curLabel==-path123) and
(curEdge.p2==self.nodes.index(vNext))) ):
found=1
vNextIndex=vNext.adjacentNodes[j]
vNext=self.nodes[vNextIndex]
if path123==1:
v.p1=v.p1+1
v.path1.append(vNextIndex)
if path123==2:
Gato/PlanarityTest.py 0100644 0017676 0017534 00000102506 10014153355 0016176 0 ustar 00schliep catbox 0000305 0050066 ################################################################################
#
# This file is part of Gato (Graph Algorithm Toolbox)
#
# file: PlanarityTest.py
# author: Ramazan Buzdemir (buzdemir@zpr.uni-koeln.de)
#
# Copyright (C) 1998-2003, Alexander Schliep, Winfried Hochstaettler and
# ZAIK/ZPR, Universitaet zu Koeln
#
# Contact: schliep@molgen.mpg.de, wh@zpr.uni-koeln.de
#
# Information: http://gato.sf.net
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Library General Public
# License as published by the Free Software Foundation; either
# version 2 of the License, or (at your option) any later version.
#
# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Library General Public License for more details.
#
# You should have received a copy of the GNU Library General Public
# License along with this library; if not, write to the Free
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#
#
#
# This file is version $Revision: 1.10 $
# from $Date: 2003/02/12 14:24:59 $
# last change by $Author: schliep $.
#
################################################################################
###############################################################################
###############################################################################
###############################################################################
# #
# AN IMPLEMENTATION OF #
# THE HOPCROFT AND TARJAN #
# PLANARITY TEST AND EMBEDDING ALGORITHM #
# #
###############################################################################
# #
# References: #
# #
# [Meh84] K.Mehlhorn. #
# "Data Structures and Efficient Algorithms." #
# Springer Verlag, 1984. #
# [MM94] K.Mehlhorn and P.Mutzel. #
# "On the embedding phase of the Hopcroft and Tarjan planarity #
# testing algorithm." #
# Technical report no. 117/94, mpi, Saarbruecken, 1994 #
# #
###############################################################################
#=============================================================================#
from copy import deepcopy
from DataStructures import Stack
from tkMessageBox import showinfo
#=============================================================================#
#=============================================================================#
class List:
def __init__(self,el=[]):
elc=deepcopy(el)
self.elements=elc
# a) Access Operations
def length(self):
return len(self.elements)
def empty(self):
if self.length()==0:
return 1
else:
return 0
def head(self):
return self.elements[0]
def tail(self):
return self.elements[-1]
# b)Update Operations
def push(self,x):
self.elements.insert(0,x)
return x
def Push(self,x):
self.elements.append(x)
return x
def append(self,x):
self.Push(x)
def pop(self):
x=self.elements[0]
self.elements=self.elements[1:]
return x
def Pop(self):
x=self.elements[-1]
self.elements=self.elements[:-1]
return x
def clear(self):
self.elements=[]
def conc(self,A):
self.elements=self.elements+A.elements
A.elements=[]
#=============================================================================#
#=============================================================================#
class pt_graph:
def __init__(self):
self.V = []
self.E = []
self.adjEdges = {}
# a) Access operations
def source(self,e):
return e[0]
def target(self,e):
return e[1]
def number_of_nodes(self):
return len(self.V)
def number_of_edges(self):
return len(self.E)
def all_nodes(self):
return self.V
def all_edges(self):
return self.E
def adj_edges(self,v):
return self.adjEdges[v]
def adj_nodes(self,v):
nodelist=[]
for e in self.adj_edges(v):
nodelist.append(e[1])
return nodelist
def first_node(self):
return self.V[0]
def last_node(self):
return self.V[-1]
def first_edge(self):
return self.E[0]
def last_edge(self):
return self.E[-1]
def first_adj_edge(self,v):
if len(self.adj_edges(v))>0:
return self.adj_edges(v)[0]
else:
return None
def last_adj_edge(self,v):
if len(self.adj_edges(v))>0:
return self.adj_edges(v)[-1]
else:
return None
# b) Update operations
def new_node(self,v):
self.V.append(v)
self.adjEdges[v]=[]
return v
def new_edge(self,v,w):
if v==w: # Loop
raise GraphNotSimpleError
if (v,w) in self.E: # Multiple edge
raise GraphNotSimpleError
self.E.append((v,w))
self.adjEdges[v].append((v,w))
return (v,w)
def del_node(self,v):
try:
for k in self.V:
for e in self.adj_edges(k):
if source(e)==v or target(e)==v:
self.adjEdges[k].remove(e)
self.V.remove(v)
for e in self.E:
if source(e)==v or target(e)==v:
self.E.remove(e)
except KeyError:
raise NoSuchVertexError
def del_edge(self,e):
try:
self.E.remove(e)
self.adjEdges[source(e)].remove((source(e),target(e)))
except KeyError:
raise NoSuchEdgeError
def del_nodes(self,node_list): # deletes all nodes in list L from self
L=deepcopy(node_list)
for l in L:
self.del_node(l)
def del_edges(self,edge_list): # deletes all edges in list L from self
L=deepcopy(edge_list)
for l in L:
self.del_edge(l)
def del_all_nodes(self): # deletes all nodes from self
self.del_nodes(self.all_nodes())
def del_all_edges(self): # deletes all edges from self
self.del_edges(self.all_edges())
def sort_edges(self, cost):
def up(x,y):
if x[1]dfsnum_w0:
self.flip()
Att.conc(self.Latt)
Att.conc(self.Ratt)
# This needs some explanation.
# Note that |Ratt| is either empty or {w0}.
# Also if |Ratt| is non-empty then all subsequent
# sets are contained in {w0}.
# So we indeed compute an ordered set of attachments.
for e in self.Lseg.elements:
alpha[e]=left
for e in self.Rseg.elements:
alpha[e]=right
#=============================================================================#
#=============================================================================#
# GLOBALS:
left=1
right=2
G=pt_graph()
reached={}
dfsnum={}
parent={}
dfs_count=0
lowpt={}
Del=[]
lowpt1={}
lowpt2={}
alpha={}
Att=List()
cur_nr=0
sort_num={}
tree_edge_into={}
#=============================================================================#
#=============================================================================#
def planarity_test(Gin):
# planarity_test decides whether the InputGraph is planar.
# it also order the adjecentLists in counterclockwise.
### SaveGmlGraph(Gin,"/home/ramazan/Leda/Graphs/rama.gml")
n=Gin.Order() # number of nodes
if n<3: return 1
if not(Gin.QDirected()) and Gin.Size()>3*n-6: return 0 # number of edges
if Gin.QDirected() and Gin.Size()>6*n-12: return 0
#--------------------------------------------------------------
# make G a copy of Gin and make G bidirected
global G,cur_nr
G=pt_graph()
for v in Gin.vertices:
G.new_node(v)
for e in Gin.Edges():
G.new_edge(source(e),target(e))
cur_nr=0
nr={}
cost={}
n=G.number_of_nodes()
for v in G.all_nodes():
nr[v]=cur_nr
cur_nr=cur_nr+1
for e in G.all_edges():
if nr[source(e)] < nr[target(e)]:
cost[e]=n*nr[source(e)] + nr[target(e)]
else:
cost[e]=n*nr[target(e)] + nr[source(e)]
G.sort_edges(cost)
L=List(G.all_edges())
while not(L.empty()):
e=L.pop()
if (not(L.empty()) and source(e)==target(L.head())
and source(L.head())==target(e)):
L.pop()
else:
G.new_edge(target(e),source(e))
#--------------------------------------------------------------
#--------------------------------------------------------------
# make G biconnected
Make_biconnected_graph()
#--------------------------------------------------------------
#--------------------------------------------------------------
# make H a copy of G
#
# We need the biconnected version of G (G will be further modified
# during the planarity test) in order to construct the planar embedding.
# So we store it as a graph H.
H=deepcopy(G)
#--------------------------------------------------------------
#--------------------------------------------------------------
# test planarity
global dfsnum,parent,alpha,Att
dfsnum={}
parent={}
for v in G.all_nodes():
parent[v]=None
reorder()
alpha={}
for e in G.all_edges():
alpha[e]=0
Att=List()
alpha[G.first_adj_edge(G.first_node())] = left
if not(strongly_planar(G.first_adj_edge(G.first_node()),Att)):
return 0
#--------------------------------------------------------------
#--------------------------------------------------------------
# construct embedding
global sort_num,tree_edge_into
T=List()
A=List()
cur_nr=0
sort_num={}
tree_edge_into={}
embedding(G.first_adj_edge(G.first_node()),left,T,A)
# |T| contains all edges incident to the first node except the
# cycle edge into it.
# That edge comprises |A|.
T.conc(A)
for e in T.elements:
sort_num[e]=cur_nr
cur_nr=cur_nr+1
H.sort_edges(sort_num)
#--------------------------------------------------------------
return H.all_edges() # ccwOrderedEges
#=============================================================================#
#=============================================================================#
def pt_DFS(v):
global G,reached
S=Stack()
if reached[v]==0:
reached[v]=1
S.Push(v)
while S.IsNotEmpty():
v=S.Pop()
for w in G.adj_nodes(v):
if reached[w]==0:
reached[w]=1
S.Push(w)
#=============================================================================#
#=============================================================================#
def Make_biconnected_graph():
# We first make it connected by linking all roots of a DFS-forest.
# Assume now that G is connected.
# Let a be any articulation point and let u and v be neighbors
# of a belonging to different biconnected components.
# Then there are embeddings of the two components with the edges
# {u,a} and {v,a} on the boundary of the unbounded face.
# Hence we may add the edge {u,v} without destroying planarity.
# Proceeding in this way we make G biconnected.
global G,reached,dfsnum,parent,dfs_count,lowpt
#--------------------------------------------------------------
# We first make G connected by linking all roots of the DFS-forest.
reached={}
for v in G.all_nodes():
reached[v]=0
u=G.first_node()
for v in G.all_nodes():
if not(reached[v]):
# explore the connected component with root v
pt_DFS(v)
if u!=v:
# link v's component to the first component
G.new_edge(u,v)
G.new_edge(v,u)
#--------------------------------------------------------------
#--------------------------------------------------------------
# We next make G biconnected.
for v in G.all_nodes():
reached[v]=0
dfsnum={}
parent={}
for v in G.all_nodes():
parent[v]=None
dfs_count=0
lowpt={}
dfs_in_make_biconnected_graph(G.first_node())
#--------------------------------------------------------------
#=============================================================================#
#=============================================================================#
def dfs_in_make_biconnected_graph(v):
# This procedure determines articulation points and adds appropriate
# edges whenever it discovers one.
global G,reached,dfsnum,parent,dfs_count,lowpt
dfsnum[v]=dfs_count
dfs_count=dfs_count+1
lowpt[v]=dfsnum[v]
reached[v]=1
if not(G.first_adj_edge(v)): return # no children
u=target(G.first_adj_edge(v)) # first child
for e in G.adj_edges(v):
w=target(e)
if not(reached[w]):
# e is a tree edge
parent[w]=v
dfs_in_make_biconnected_graph(w)
if lowpt[w]==dfsnum[v]:
# v is an articulation point. We now add an edge.
# If w is the first child and v has a parent then we
# connect w and parent[v], if w is a first child and v
# has no parent then we do nothing.
# If w is not the first child then we connect w to the
# first child.
# The net effect of all of this is to link all children
# of an articulation point to the first child and the
# first child to the parent (if it exists).
if w==u and parent[v]:
G.new_edge(w,parent[v])
G.new_edge(parent[v],w)
if w!=u:
G.new_edge(u,w)
G.new_edge(w,u)
lowpt[v]=min(lowpt[v],lowpt[w])
else:
lowpt[v]=min(lowpt[v],dfsnum[w]) # non tree edge
#=============================================================================#
#=============================================================================#
def reorder():
# The procedure reorder first performs DFS to compute dfsnum, parent
# lowpt1 and lowpt2, and the list Del of all forward edges and all
# reversals of tree edges.
# It then deletes the edges in Del and finally reorders the edges.
global G,dfsnum,parent,reached,dfs_count,Del,lowpt1,lowpt2
reached={}
for v in G.all_nodes():
reached[v]=0
dfs_count = 0
Del=[]
lowpt1={}
lowpt2={}
dfs_in_reorder(G.first_node())
#--------------------------------------------------------------
# remove forward and reversals of tree edges
for e in Del:
G.del_edge(e)
#--------------------------------------------------------------
#--------------------------------------------------------------
# we now reorder adjacency lists
cost={}
for e in G.all_edges():
v = source(e)
w = target(e)
if dfsnum[w]=dfsnum[v]:
cost[e]=2*lowpt1[w]
else:
cost[e]=2*lowpt1[w]+1
G.sort_edges(cost)
#--------------------------------------------------------------
#=============================================================================#
#=============================================================================#
def dfs_in_reorder(v):
global G,dfsnum,parent,reached,dfs_count,Del,lowpt1,lowpt2
#--------------------------------------------------------------
dfsnum[v]=dfs_count
dfs_count=dfs_count+1
lowpt1[v]=lowpt2[v]=dfsnum[v]
reached[v]=1
for e in G.adj_edges(v):
w = target(e);
if not(reached[w]):
# e is a tree edge
parent[w]=v
dfs_in_reorder(w)
lowpt1[v]=min(lowpt1[v],lowpt1[w])
else:
lowpt1[v]=min(lowpt1[v],dfsnum[w]) # no effect for forward edges
if dfsnum[w]>=dfsnum[v] or w==parent[v]:
# forward edge or reversal of tree edge
Del.append(e)
#--------------------------------------------------------------
#--------------------------------------------------------------
# we know |lowpt1[v]| at this point and now make a second pass over all
# adjacent edges of |v| to compute |lowpt2|
for e in G.adj_edges(v):
w = target(e)
if parent[w]==v:
# tree edge
if lowpt1[w]!=lowpt1[v]:
lowpt2[v]=min(lowpt2[v],lowpt1[w])
lowpt2[v]=min(lowpt2[v],lowpt2[w])
else:
# all other edges
if lowpt1[v]!=dfsnum[w]:
lowpt2[v]=min(lowpt2[v],dfsnum[w])
#--------------------------------------------------------------
#=============================================================================#
#=============================================================================#
def strongly_planar(e0,Att):
# We now come to the heart of the planarity test: procedure strongly_planar.
# It takes a tree edge e0=(x,y) and tests whether the segment S(e0) is
# strongly planar.
# If successful it returns (in Att) the ordered list of attachments of S(e0)
# (excluding x); high DFS-numbers are at the front of the list.
# In alpha it records the placement of the subsegments.
#
# strongly_planar operates in three phases.
# It first constructs the cycle C(e0) underlying the segment S(e0).
# It then constructs the interlacing graph for the segments emanating >from the
# spine of the cycle.
# If this graph is non-bipartite then the segment S(e0) is non-planar.
# If it is bipartite then the segment is planar.
# In this case the third phase checks whether the segment is strongly planar
# and, if so, computes its list of attachments.
global G,alpha,dfsnum,parent
#--------------------------------------------------------------
# DETERMINE THE CYCLE C(e0)
# We determine the cycle "C(e0)" by following first edges until a back
# edge is encountered.
# |wk| will be the last node on the tree path and |w0|
# is the destination of the back edge.
x=source(e0)
y=target(e0)
e=G.first_adj_edge(y)
wk=y
while dfsnum[target(e)]>dfsnum[wk]: # e is a tree edge
wk=target(e)
e=G.first_adj_edge(wk)
w0=target(e)
#--------------------------------------------------------------
#--------------------------------------------------------------
# PROCESS ALL EDGES LEAVING THE SPINE
# The second phase of |strongly_planar| constructs the connected
# components of the interlacing graph of the segments emananating
# from the the spine of the cycle "C(e0)".
# We call a connected component a "block".
# For each block we store the segments comprising its left and
# right side (lists |Lseg| and |Rseg| contain the edges defining
# these segments) and the ordered list of attachments of the segments
# in the block;
# lists |Latt| and |Ratt| contain the DFS-numbers of the attachments;
# high DFS-numbers are at the front of the list.
#
# We process the edges leaving the spine of "S(e0)" starting at
# node |wk| and working backwards.
# The interlacing graph of the segments emanating from
# the cycle is represented as a stack |S| of blocks.
w=wk
S=Stack()
while w!=x:
count=0
for e in G.adj_edges(w):
count=count+1
if count!=1: # no action for first edge
# TEST RECURSIVELY
# Let "e" be any edge leaving the spine.
# We need to test whether "S(e)" is strongly planar
# and if so compute its list |A| of attachments.
# If "e" is a tree edge we call our procedure recursively
# and if "e" is a back edge then "S(e)" is certainly strongly
# planar and |target(e)| is the only attachment.
# If we detect non-planarity we return false and free
# the storage allocated for the blocks of stack |S|.
A=List()
if dfsnum[w]dfsnum[w0] and B.head_of_Ratt()>dfsnum[w0]):
del B
while S.IsNotEmpty(): S.Pop()
return 0
B.add_to_Att(Att,dfsnum[w0],alpha,dfsnum)
del B
# Let's not forget that "w0" is an attachment
# of "S(e0)" except if w0 = x.
if w0!=x: Att.append(dfsnum[w0])
return 1
#--------------------------------------------------------------
#=============================================================================#
#=============================================================================#
def embedding(e0,t,T,A):
# embed: determine the cycle "C(e0)"
#
# We start by determining the spine cycle.
# This is precisley as in |strongly_planar|.
# We also record for the vertices w_r+1, ...,w_k, and w_0 the
# incoming cycle edge either in |tree_edge_into| or in the local
# variable |back_edge_into_w0|.
global G,dfsnum,cur_nr,sort_num,tree_edge_into,parent
x=source(e0)
y=target(e0)
tree_edge_into[y]=e0
e=G.first_adj_edge(y)
wk=y
while (dfsnum[target(e)]>dfsnum[wk]): # e is a tree edge
wk=target(e)
tree_edge_into[wk]=e
e=G.first_adj_edge(wk)
w0=target(e)
back_edge_into_w0=e
# process the subsegments
w=wk
Al=List()
Ar=List()
Tprime=List()
Aprime=List()
T.clear()
T.append(e) # |e=(wk,w0)| at this point
while w!=x:
count=0
for e in G.adj_edges(w):
count=count+1
if count!=1: # no action for first edge
# embed recursively
if dfsnum[w][%i]" %((source(e)-1),(target(e)-1))
print
for v in G.all_nodes():
print "[%i] : " %(v-1)
for e in G.adj_edges(v):
print " [%i]---->[%i]" %((source(e)-1),(target(e)-1))
print
#=============================================================================#
#=============================================================================#
def SaveGmlGraph(G, fileName):
file = open(fileName, 'w')
file.write("graph [ \n")
file.write(" directed 1 \n \n")
for v in G.vertices:
file.write(" node [ id ")
n=str(v)
file.write(n)
file.write(" ] \n")
file.write("\n")
for e in G.Edges():
file.write(" edge [ \n")
file.write(" source ")
k=str(e[0])
file.write(k)
file.write("\n")
file.write(" target ")
k=str(e[1])
file.write(k)
file.write("\n")
file.write(" ] \n")
file.write("] \n")
#=============================================================================#
segments emananating
# from the the spine of the cycle "C(e0)".
# We call a connected component a "block".
# For each block we store the segments comprising its left and
Gato/TextTreeWidget.py 0100644 0017676 0017534 00000036771 10014153355 0016315 0 ustar 00schliep catbox 0000305 0050066 #!/usr/bin/env python2.2
################################################################################
#
# This file is part of Gato (Graph Animation Toolbox)
#
# file: GatoFile.py
# author: Achim Gaedke (achim.gaedke@zpr.uni-koeln.de)
#
# Copyright (C) 1998-2003, Alexander Schliep, Winfried Hochstaettler and
# ZAIK/ZPR, Universitaet zu Koeln
#
# Contact: schliep@molgen.mpg.de, wh@zpr.uni-koeln.de
# Information: http://gato.sf.net
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Library General Public
# License as published by the Free Software Foundation; either
# version 2 of the License, or (at your option) any later version.
#
# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Library General Public License for more details.
#
# You should have received a copy of the GNU Library General Public
# License along with this library; if not, write to the Free
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#
# This file is version $Revision: 1.3 $
# from $Date: 2003/02/12 14:25:00 $
# last change by $Author: schliep $.
#
################################################################################
import Tkinter
class dom_structure_widget(Tkinter.Frame):
"""
this widget displays the structure of a dom tree
"""
class nodeDisplayProperties:
"""
holds all properties for node
"""
def __init__(self):
self.expanded=0
self.fg_color=""
self.bg_color=""
self.tag=""
def __init__(self,master,
dom,
report_function=None,
cnf={},
**config):
"""
creates the dom tree widget
"""
# open a text widget
my_defaults={"highlightthickness":"0"}
my_defaults.update(config)
my_defaults.update(cnf)
if my_defaults.has_key("font"): del my_defaults["font"]
text_defaults={'bg':'white','cursor':'top_left_arrow',
'selectbackground':"white", 'selectborderwidth':'0',
"insertwidth":"0", "exportselection":"0",
"highlightthickness":"0"}
text_defaults.update(config)
text_defaults.update(cnf)
Tkinter.Frame.__init__(self,master,cnf=my_defaults)
self.textWidget=Tkinter.Text(self,cnf=text_defaults)
self.textWidget.pack(side=Tkinter.LEFT,fill=Tkinter.BOTH,expand=1)
self.scroll=Tkinter.Scrollbar(self,command=self.textWidget.yview)
self.textWidget.configure(yscrollcommand=self.scroll.set)
self.scroll.pack(side=Tkinter.LEFT,fill=Tkinter.Y,expand=1)
self.dom=dom
self.selectedNode=None
self.report_function=report_function
self.indent_step=" "
self.expand_sign="+"
self.collapse_sign="-"
#parameters for default isVisible function
self.mergeText = 0 # if contiguous text nodes should be merged
self.textTypes = [xml.dom.Node.TEXT_NODE, xml.dom.Node.CDATA_SECTION_NODE]
self.ignorableTypes = self.textTypes # ignore text by default
# without normal state no key events
self.textWidget.configure(state=Tkinter.NORMAL)
# start with root element
self.textWidget.mark_set(Tkinter.INSERT,Tkinter.END)
self.textWidget.mark_gravity(Tkinter.INSERT,Tkinter.RIGHT)
self.createNodeEntries("root",dom,"",Tkinter.INSERT,-1)
# enable node selection
self.selectedTag=None
self.textWidget.tag_bind("collapse","",self.collapseNodeEvent)
self.textWidget.tag_bind("expand","",self.expandNodeEvent)
self.textWidget.tag_bind("node","",self.selectNodeEvent)
self.textWidget.tag_bind("node","",self.chooseNodeEvent)
self.textWidget.bind("",lambda e:"break")
self.textWidget.bind("" , self.prevNodeEvent)
self.textWidget.bind("", self.nextNodeEvent)
self.textWidget.bind("" , self.collapseNodeEvent)
self.textWidget.bind("", self.expandNodeEvent)
self.textWidget.bind("",self.chooseNodeEvent)
def isVisible(self, node):
"""
default function for visibility of nodes
"""
# simple case: ignore all types of this case
if node.nodeType in self.ignorableTypes:
return 0
# merge subsequent text nodes and display only first.
if self.mergeText and node.nodeType in self.textTypes:
lastNode=node.previousSibling
if lastNode:
return lastNode.nodeType in self.textTypes
else:
return 1
return 1
def nameNode(self, node):
return xmlDecode(node.nodeName)
def setNodeColor(self, node, fg="", bg=""):
"""
sets the color of a node
"""
if not node.__dict__.has_key("DisplayProperties"):
node.DisplayProperties=dom_structure_widget.nodeDisplayProperties()
if fg:
node.DisplayProperties.fg_color=fg
if bg:
node.DisplayProperties.bg_color=bg
if node.DisplayProperties.tag:
self.textWidget.tag_config("nodeName-"+node.DisplayProperties.tag,
foreground=node.DisplayProperties.fg_color,
background=node.DisplayProperties.bg_color)
def createNodeEntries(self,subtag,subtree,indentation,mark_name,depth=-1):
"""
recursive creation of all node entries
"""
if depth==0: return
i=0 # counter of nodes to identifiy the label
if not subtree.__dict__.has_key("DisplayProperties"):
subtree.DisplayProperties=dom_structure_widget.nodeDisplayProperties()
subtree.DisplayProperties.expanded=1
for node in subtree.childNodes:
# select nodes that are displayed
if not self.isVisible(node):
# do not display it
i+=1
continue
this_subtag="%s-%d"%(subtag,i)
# node will be displayed, so add property tag
if not node.__dict__.has_key("DisplayProperties"):
node.DisplayProperties=dom_structure_widget.nodeDisplayProperties()
node.DisplayProperties.tag=this_subtag
self.textWidget.tag_config("nodeName-"+this_subtag,
foreground=node.DisplayProperties.fg_color,
background=node.DisplayProperties.bg_color)
# has this node visible children ? -> is it expandable ?
if node.hasChildNodes() and filter(self.isVisible,node.childNodes):
# yes, it has children
# decide if node is expanded or not
if depth==1:
node.DisplayProperties.expanded=0
elif depth>1:
node.DisplayProperties.expanded=1
# otherwise, do it as done last time
# indent
self.textWidget.insert(mark_name, indentation, this_subtag)
# draw node
if node.DisplayProperties.expanded:
self.textWidget.insert(mark_name,self.collapse_sign,(this_subtag,"collapse"))
else:
self.textWidget.insert(mark_name,self.expand_sign,(this_subtag,"expand"))
# write name
self.textWidget.insert(mark_name,
self.nameNode(node),
(this_subtag,"node","nodeName-"+this_subtag))
# write end of line
self.textWidget.insert(mark_name, "\n", this_subtag)
if node.DisplayProperties.expanded and depth!=1:
# one more level to write
self.createNodeEntries(this_subtag,
node,
indentation+self.indent_step,
mark_name,
depth-1)
else:
# no, no children
# write indentation and no collapse/expand handle
self.textWidget.insert(mark_name, indentation+" ", this_subtag)
# now name
self.textWidget.insert(mark_name,
self.nameNode(node),
(this_subtag,"node","nodeName-"+this_subtag))
# now end of line
self.textWidget.insert(mark_name, "\n", this_subtag)
i+=1
def collapseNodeEvent(self,event):
"""
find tag, that denotes the subtree and pass to collapse_tag
"""
if event.type=="4":
# collapse sign hit by mouse
my_tags=self.textWidget.tag_names("@%d,%d"%(event.x,event.y))
tree_tags=filter(lambda t:t[:4]=='root',my_tags)
if len(tree_tags)!=1:
print "not good: found ",tree_tags,"to collapse!"
return "break"
self.collapse_tag(tree_tags[0])
return "break"
elif event.type=="2":
# left arrow key, ToDo
return "break"
else:
return
def expandNodeEvent(self,event):
"""
find tag, that denotes the subtree and pass to collapse_tag
"""
if event.type=="4":
# collapse sign hit by mouse
my_tags=self.textWidget.tag_names("@%d,%d"%(event.x,event.y))
tree_tag=filter(lambda t:t[:4]=='root',my_tags)
if len(tree_tag)!=1:
print "not good: found ",tree_tag," to expand!"
return "break"
self.expand_tag(tree_tag[0])
return "break"
elif event.type=="2":
# right arrow key, ToDo
return "break"
else:
return
def selectNodeEvent(self,event):
"""
clicked once on the node
"""
my_tags=self.textWidget.tag_names("@%d,%d"%(event.x,event.y))
tree_tags=filter(lambda t:t[:4]=='root',my_tags)
if len(tree_tags)!=1:
print "not good: found ",tree_tags," to select!"
subtree=self.subtree_from_tag(tree_tags[0],self.dom)
# mark the node
self.selectNode(tree_tags[0])
return "break"
def nextNodeEvent(self,event):
"""
event for down key
"""
if self.selectedNode is None: return "break"
(start,end)=self.textWidget.tag_ranges(self.selectedNode.DisplayProperties.tag)
result=self.textWidget.tag_nextrange("node",end)
if len(result)==0: return "break"
my_tags=self.textWidget.tag_names(result[0])
tree_tags=filter(lambda t:t[:4]=='root',my_tags)
if len(tree_tags)!=1:
print "not good: found ",tree_tags," to select!"
# mark the node
self.selectNode(tree_tags[0])
return "break"
def prevNodeEvent(self,event):
"""
event for up key
"""
if self.selectedNode is None: return "break"
(start,end)=self.textWidget.tag_ranges(self.selectedNode.DisplayProperties.tag)
result=self.textWidget.tag_prevrange("node",start)
if len(result)==0: return "break"
my_tags=self.textWidget.tag_names(result[0])
tree_tags=filter(lambda t:t[:4]=='root',my_tags)
if len(tree_tags)!=1:
print "not good: found ",tree_tags," to select!"
# mark the node
self.selectNode(tree_tags[0])
return "break"
def selectNode(self,tree_tag):
"""
mark node as selected
the node is specified by the tree tag
"""
if self.selectedNode:
self.setNodeColor(self.selectedNode,bg="white")
# find node element for this line
subtree=self.subtree_from_tag(tree_tag,self.dom)
self.setNodeColor(subtree,bg="green")
self.selectedNode=subtree
if self.report_function and subtree:
self.report_function("selectedNode",subtree)
def chooseNodeEvent(self,event):
"""
choose node: i.e. report to report function
"""
subtree=None
if event.type=="4":
# selection by mouse
my_tags=self.textWidget.tag_names("@%d,%d"%(event.x,event.y))
tree_tags=filter(lambda t:t[:4]=='root',my_tags)
if len(tree_tags)!=1:
print "not good: found ",tree_tags," to select!"
# select this node
self.selectNode(tree_tags[0])
subtree=self.subtree_from_tag(tree_tags[0],self.dom)
elif event.type=="2":
# selection by key
if self.selectedNode is None:
return "break"
subtree=self.selectedNode
else:
# unknown
return "break"
if self.report_function and subtree:
self.report_function("chosenNode",subtree)
return "break"
def collapse_tag(self,tag):
"""
collapses the tree under the given tag
"""
subtree=self.subtree_from_tag(tag,self.dom)
subtree.DisplayProperties.expanded=0
all_tags=self.textWidget.tag_names()
# determine tags to delete
tags_to_delete=filter(lambda s,t=tag+"-",l=len(tag)+1:s[:l]==t,all_tags)
# handle selected node, that may become invisible
if self.selectedNode is not None and self.selectedNode.DisplayProperties.tag in tags_to_delete:
self.selectNode(tag)
for t in tags_to_delete:
self.textWidget.delete(t+".first",t+".last")
self.textWidget.tag_delete(t)
# change collapse symbol to expand symbol
symbol_index=self.textWidget.tag_nextrange('collapse',tag+".first",tag+".last")
self.textWidget.delete(symbol_index[0],symbol_index[1])
self.textWidget.insert(symbol_index[0],self.expand_sign,('expand',tag))
def expand_tag(self,tag):
"""
expand the tree under the tag
"""
subtree=self.subtree_from_tag(tag,self.dom)
start_index=self.textWidget.index(tag+".last")
self.textWidget.mark_set(Tkinter.INSERT,start_index)
# change expand symbol to collapse symbol
symbol_index=self.textWidget.tag_nextrange('expand',tag+".first",tag+".last")
symbol_index=self.textWidget.tag_nextrange('expand',tag+".first",tag+".last")
self.textWidget.delete(symbol_index[0],symbol_index[1])
self.textWidget.insert(symbol_index[0],self.collapse_sign,('collapse',tag))
# determine indentation
indentation=self.textWidget.get(tag+".first",symbol_index[0])
self.createNodeEntries(tag,
subtree,
indentation+self.indent_step,
Tkinter.INSERT,
-1)
def subtree_from_tag(self,tag,dom):
"""
parse tag and traverse tree
"""
subtree=dom
for token in tag.split('-'):
if token=='root':
subtree=dom
else:
index_nr=int(token)
subtree=subtree.childNodes[index_nr]
return subtree
seGato/TreeWidget.py 0100644 0017676 0017534 00000060011 10014153355 0015430 0 ustar 00schliep catbox 0000305 0050066 ################################################################################
#
# This file is part of Gato (Graph Animation Toolbox)
#
# file: GatoFile.py
# author: Achim Gaedke (achim.gaedke@zpr.uni-koeln.de)
#
# Copyright (C) 1998-2003, Alexander Schliep, Winfried Hochstaettler and
# ZAIK/ZPR, Universitaet zu Koeln
#
# Contact: schliep@molgen.mpg.de, wh@zpr.uni-koeln.de
# Information: http://gato.sf.net
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Library General Public
# License as published by the Free Software Foundation; either
# version 2 of the License, or (at your option) any later version.
#
# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Library General Public License for more details.
#
# You should have received a copy of the GNU Library General Public
# License along with this library; if not, write to the Free
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#
# This file is version $Revision: 1.4 $
# from $Date: 2003/02/12 14:25:00 $
# last change by $Author: schliep $.
#
################################################################################
import Tkinter
import sys
import os
import os.path
import codecs
import xml.dom
import xml.dom.minidom
class Node:
"""
base class for all elements of my tree, provides icon support, selection features
"""
def __init__(self,parent=None, name=None, icon=None, anchor=(0,0)):
"""
initialises the node
"""
# create the default image once....
self.setParent(parent)
self.name=name
self.icon=icon
self.anchor=anchor
self.selectable=0
self.selected=0
self.selectionItem=None
# cached item tags
self.canvas=self.nameItem=self.iconItem=None
def isVisible(self):
"""
ask, if this node is displayed
"""
if self.canvas:
return 1
else:
return 0
def display(self,recursive=0):
"""
display this node, all components are displayed or reconfigured
if recursive is 0, the tail of the tree will be reordered
"""
self.canvas=self.parent.getCanvas()
# the parent is not displayed, so we do not display ourself
if self.canvas is None:
return
nx0=nx1=ix0=ix1=self.anchor[0]
ny0=ny1=iy0=iy1=self.anchor[1]
# display or update icon
if self.icon:
if not self.iconItem:
self.iconItem=self.canvas.create_image(self.anchor,
anchor=Tkinter.NW,
image=self.icon)
else:
self.canvas.itemconfigure(self.iconItem,image=self.icon)
(ix0,iy0,ix1,iy1)=self.canvas.bbox(self.iconItem)
self.canvas.tag_bind(self.iconItem,"",self.selectionCallback)
else:
if self.iconItem:
self.canvas.tag_unbind(self.iconItem)
self.canvas.delete(self.iconItem)
self.iconItem=None
# display or update text after icon...
if self.name:
if not self.nameItem:
self.nameItem=self.canvas.create_text((ix1+1,self.anchor[1]),
anchor=Tkinter.NW,
text=self.name)
else:
self.canvas.itemconfigure(self.nameItem, text=self.name)
(nx0,ny0,nx1,ny1)=self.canvas.bbox(self.nameItem)
self.canvas.tag_bind(self.nameItem,"",self.selectionCallback)
else:
if self.nameItem:
self.canvas.tag_unbind(self.nameItem)
self.canvas.delete(self.nameItem)
self.nameItem=None
# look whether icon is taller than text
diff=iy1-iy0-ny1+ny0
if diff>0 and self.nameItem:
# center text
self.canvas.coords(self.nameItem,(nx0,(iy0+iy1)/2))
self.canvas.itemconfigure(self.nameItem,anchor=Tkinter.W)
elif diff<0 and self.iconItem:
# center icon
self.canvas.coords(self.iconItem,(ix0,(ny0+ny1)/2))
self.canvas.itemconfigure(self.iconItem,anchor=Tkinter.W)
# maintain selection
if self.selected:
coords=apply(self.canvas.bbox,
filter(None,[self.iconItem,self.nameItem]))
if self.selectionItem:
self.canvas.coords(self.selectionItem,coords)
else:
self.selectionItem=self.canvas.create_rectangle(coords,outline="",fill="green")
self.canvas.lower(self.selectionItem)
else:
if self.selectionItem:
self.canvas.delete(self.selectionItem)
self.selectionItem=None
# only top call should reorder the tree
if recursive==0:
self.parent.moveAfterChild(self)
def update(self,recursive=0):
"""
update the icon and name
"""
if self.isVisible():
return self.display(recursive)
else:
return None
def conceal(self,recursive=0):
"""
conceal this node from tree, destruct all icons and text
if recursive is 0, the tail of the tree will be reordered
"""
# we are not displayed, do nothing
if not self.canvas:
return
if self.nameItem:
self.canvas.tag_unbind(self.nameItem,"")
self.canvas.delete(self.nameItem)
self.nameItem=None
if self.iconItem:
self.canvas.tag_unbind(self.iconItem,"")
self.canvas.delete(self.iconItem)
self.iconItem=None
if self.selectionItem:
self.canvas.delete(self.selectionItem)
self.selectionItem=None
self.canvas=None
# only top call should reorder the tree
if recursive==0:
self.parent.moveAfterChild(self)
def getBoundingBox(self):
# we are not displayed
if self.canvas is None:
return None
items=self.getAllItems()
if items:
return apply(self.canvas.bbox,items)
else:
return (self.anchor[0],self.anchor[1],self.anchor[0],self.anchor[1])
def getAllItems(self):
items=[]
if self.nameItem:
items.append(self.nameItem)
if self.iconItem:
items.append(self.iconItem)
if self.selectionItem:
items.append(self.selectionItem)
return items
def getNamePath(self):
parentPath=self.parent.getNamePath()
parentPath.append(self.name)
return parentPath
def getCanvas(self):
"""
get canvas from parent
"""
return self.canvas
def setParent(self,newParent):
"""
sets own parent
"""
self.parent=newParent
def select(self):
"""
show selection highligting
"""
self.selected=1
if self.canvas:
coords=apply(self.canvas.bbox,
filter(None,[self.iconItem,self.nameItem]))
if self.selectionItem:
self.canvas.coords(self.selectionItem,coords)
else:
self.selectionItem=self.canvas.create_rectangle(coords,outline="",fill="green")
self.canvas.lower(self.selectionItem)
def deselect(self):
self.selected=0
if self.canvas and self.selectionItem:
self.canvas.delete(self.selectionItem)
self.selectionItem=None
def selectionCallback(self,event):
"""
"""
if not self.selectable:
return
if not self.selected:
self.select()
else:
self.deselect()
def printNode(self,indent=""):
refcnt=sys.getrefcount(self)
print "%s%s:%d"%(indent,self.name,refcnt)
class Leaf(Node):
"""
the leaf cannot contain children.
"""
defaultIconData="R0lGODlhDAAMAKEAALLA3AAAAP//8wAAACH5BAEAAAAALAAAAAAMAAwAAAIgRI4Ha+IfWHsOrSASvJTGhnhcV3EJlo3kh53ltF5nAhQAOw=="
def __init__(self,parent=None, anchor=(0,0), name=None, icon=None):
if not Leaf.__dict__.has_key("defaultIcon"):
Leaf.defaultIcon=Tkinter.PhotoImage(data=Leaf.defaultIconData)
if name==None:
raise Exception("name should be text")
if not icon:
icon=Leaf.defaultIcon
Node.__init__(self,parent=parent, name=name, anchor=anchor, icon=icon)
class Branch(Node):
"""
the branch contains leafs or branches
it can be expanded and collapsed and maintains its children
"""
expandData="""
#define plus_width 13
#define plus_height 13
static unsigned char plus_bits[] = {
0x00, 0x00, 0xfe, 0x0f, 0x02, 0x08, 0x42, 0x08, 0x42, 0x08, 0x42, 0x08,
0xfa, 0x0b, 0x42, 0x08, 0x42, 0x08, 0x42, 0x08, 0x02, 0x08, 0xfe, 0x0f,
0x00, 0x00};"""
collapseData="""
#define minus_width 13
#define minus_height 13
static unsigned char minus_bits[] = {
0x00, 0x00, 0xfe, 0x0f, 0x02, 0x08, 0x02, 0x08, 0x02, 0x08, 0x02, 0x08,
0xfa, 0x0b, 0x02, 0x08, 0x02, 0x08, 0x02, 0x08, 0x02, 0x08, 0xfe, 0x0f,
0x00, 0x00};
"""
def __init__(self,parent=None, anchor=(0,0), name=None, expanded=0, children=[]):
"""
initialises a branch:
children can be specified...
"""
# initialise Icon Data
if not Branch.__dict__.has_key("expandImage"):
Branch.expandImage=Tkinter.BitmapImage(data=Branch.expandData)
if not Branch.__dict__.has_key("collapseImage"):
Branch.collapseImage=Tkinter.BitmapImage(data=Branch.collapseData)
self.expanded=expanded
icon=Branch.expandImage
if self.expanded:
icon=self.collapseImage
self.children=children
Node.__init__(self, parent=parent, anchor=anchor, name=name, icon=icon)
def expand(self):
"""
Special feature of a branch: can display all direct children
before expanding the childlist is updated
"""
self.expanded=1
self.update()
def collapse(self):
"""
Special feature of a branch: can conceal all children on demand
after collapsing, the childlist is cleaned up
"""
self.expanded=0
self.update()
def updateChildList(self):
"""
called before expanding the branch in order to update all necessary children
"""
pass
def cleanupChildList(self):
"""
called after collapsed the branch in order to destruct all unused children
"""
pass
def display(self,recursive=0):
"""
set expand/collapse handle, care about children and return....
"""
self.canvas=self.parent.getCanvas()
# parent is not displayed
if self.canvas is None:
return
if self.expanded:
self.icon=self.collapseImage
Node.display(self,recursive+1)
(x0,y0,x1,y1)=apply(self.canvas.bbox,filter(None,[self.nameItem,self.iconItem]))
(ix0,iy0,ix1,iy1)=self.canvas.bbox(self.iconItem)
self.updateChildList()
for child in self.children:
child.anchor=(ix1+1,y1+1)
child.display(recursive+1)
(x0,y0,x1,y1)=child.getBoundingBox()
else:
self.icon=self.expandImage
Node.display(self,recursive+1)
for child in self.children:
child.conceal(recursive+1)
self.cleanupChildList()
# now take care about the rest...
self.canvas.tag_bind(self.iconItem,"", self.toogleCallback)
if recursive==0:
self.parent.moveAfterChild(self)
def conceal(self,recursive):
"""
conceal all children and self
"""
for child in self.children:
child.conceal(recursive+1)
self.cleanupChildList()
Node.conceal(self,recursive)
def getAllItems(self):
"""
get all canvas items, that contribute to the displayed branch
"""
items=Node.getAllItems(self)
for child in self.children:
items+=child.getAllItems()
return items
def moveAfterChild(self, child):
"""
recursive move mechanism in order to replace all children beneath the calling child
"""
if not self.isVisible():
self.parent.moveAfterChild(self)
return
# look for calling child position
idx=self.children.index(child)
# if this was the last one...
if idx>=len(self.children):
self.parent.moveAfterChild(self)
return
# search for precedent visible child
lastIdx=idx
while lastIdx>=0 and not self.children[lastIdx].isVisible():
lastIdx-=1
if lastIdx<0:
self.parent.moveAfterChild(self)
return
# determine new position
(nx1,ny1,nx2,ny2)=self.children[lastIdx].getBoundingBox()
# look for child after calling child
# and get succeding tags to move..
idx+=1
items=[]
while idx