graphed.py :  » Development » Leo » Leo-4.7.1-final » leo » plugins » Python Open Source

Home
Python Open Source
1.3.1.2 Python
2.Ajax
3.Aspect Oriented
4.Blog
5.Build
6.Business Application
7.Chart Report
8.Content Management Systems
9.Cryptographic
10.Database
11.Development
12.Editor
13.Email
14.ERP
15.Game 2D 3D
16.GIS
17.GUI
18.IDE
19.Installer
20.IRC
21.Issue Tracker
22.Language Interface
23.Log
24.Math
25.Media Sound Audio
26.Mobile
27.Network
28.Parser
29.PDF
30.Project Management
31.RSS
32.Search
33.Security
34.Template Engines
35.Test
36.UML
37.USB Serial
38.Web Frameworks
39.Web Server
40.Web Services
41.Web Unit
42.Wiki
43.Windows
44.XML
Python Open Source » Development » Leo 
Leo » Leo 4.7.1 final » leo » plugins » graphed.py
#@+leo-ver=4-thin
#@+node:ekr.20071004090250:@thin graphed.py
#@<< docstring >>
#@+node:ekr.20071004090250.1:<< docstring >>
"""
graphed.py  -- Edit graphs visually

Graph commands are in the Outline/Graph submenu.
See http://leo.zwiki.org/GraphEd for documentation.

Graph editor component based on the Gred graph editor from the
Gato Graph Animation Toolbox at http://gato.sourceforge.net/

Leo plugin by Terry Brown terry_n_brown@yahoo.com
"""
#@-node:ekr.20071004090250.1:<< docstring >>
#@nl

#@@language python
#@@tabwidth -4
#@@nowrap

__version__ = "0.4"

#@<< imports >>
#@+node:ekr.20071004090250.2:<< imports >>
import leo.core.leoGlobals as g
import leo.core.leoPlugins as leoPlugins

Tk = g.importExtension('Tkinter',pluginName=__name__,verbose=True)

import sys

gato_path = g.os_path_join(g.app.loadDir,'..','extensions','Gato')

if gato_path not in sys.path:
    sys.path.append(gato_path)

try:
    from Gato import Gred,Graph
    Gato_ok = True
except:
    Gato_ok = False
#@-node:ekr.20071004090250.2:<< imports >>
#@nl
#@<< version history >>
#@+node:ekr.20071004090250.3:<< version history >>
#@@killcolor

#@+at 
#@nonl
# Use and distribute under the same terms as leo itself.
# 
# 0.0 - initial version
# 
# 0.1 EKR:
# - reassigned all gnx's (by cutting and pasting the
# entire @thin node) to avoid conflict with cleo (!!)
# 
# - Add leo/extensions/Gato to sys.path before importing from Gato.
# 0.2 TNB:
#   - moved leo <-> graph stuff into separate class, much tidier not
#     mixing that with Gato stuff
#   - put x,y uAs on tnodes rather than vnodes because low level graph
#     class is a graph of tnodes (gnxs)
#   - implemented dot export functions
# 0.3 TNB:
#   - cleo background colors used in editor and graphviz export
#   - gNode class used to make API more friendly
#   - ask user if they want headings split into lines on spaces
#   - @graph headString text to indicate container node
# 0.4 EKR: changed p.link to p._link, and p.unlink to p._unlink
#@-at
#@-node:ekr.20071004090250.3:<< version history >>
#@nl

#@+others
#@+node:ekr.20071004090250.9:init
def init():

    if not Gato_ok:
        g.es('graphed: Gato import failed, functions reduced',color='red')

    leoPlugins.registerHandler('after-create-leo-frame', onCreate)
    g.plugin_signon(__name__)

    return True
#@-node:ekr.20071004090250.9:init
#@+node:ekr.20071004090250.10:onCreate
def onCreate (tag,key):
    GraphEd(key['c'])
#@-node:ekr.20071004090250.10:onCreate
#@+node:tbrown.20071007201356:class gNode
class gNode(object):
    """Simple graph node.  Acts as a dictionary, although not by descent, as
     it needs to be hashable.

     Special attributes (not in dict):
       - x x-coord of node in graph
       - y y-coord of node in graph
       - title label for node in graph, headString for leo node
       - body bodText for leo node
     """
    #@    @+others
    #@+node:tbrown.20071007213346:__init__
    def __init__(self, *args, **kwds):
        self._dict = {}
        self.x = self.y = 10
        self.body = self.label = ""
        self.attr = {}
    #@-node:tbrown.20071007213346:__init__
    #@+node:tbrown.20071007213346.1:readtnode
    def readtnode(self, tn, splitLabels = True, vnode = None):
        if splitLabels:
            self.title = tn.h.replace(' ', '\n')
        else:
            self.title = tn.h
        self.body = tn.b
        self.x = getattr(tn,'unknownAttributes',{}).get('graphed',{}).get('x',0)
        self.y = getattr(tn,'unknownAttributes',{}).get('graphed',{}).get('y',0)
        self.attr.update(getattr(tn,'unknownAttributes',{}))
        self.vnode = vnode
    #@-node:tbrown.20071007213346.1:readtnode
    #@+node:tbrown.20071007220740:writetnode
    def writetnode(self, nd, vnode = None):
        nd.setHeadString(self.title.replace('\n', ' '))
        nd.setTnodeText(self.body)
        if self.attr:
            if not hasattr(nd,'unknownAttributes'): nd.unknownAttributes = {}
            nd.unknownAttributes.update(self.attr)
        if hasattr(self, 'vnode'):
            if self.vnode != None and vnode != None:
                if not hasattr(vnode,'unknownAttributes'):
                    vnode.unknownAttributes = {}
                vnode.unknownAttributes.update(
                    getattr(self.vnode, 'unknownAttributes', {}))
        if (hasattr(self, 'x') and hasattr(self, 'y')
            and self.x != 0 or self.y != 0):
            if not hasattr(nd,'unknownAttributes'): nd.unknownAttributes = {}
            nd.unknownAttributes.setdefault('graphed',{}).update(
                {'x': self.x, 'y': self.y})
    #@-node:tbrown.20071007220740:writetnode
    #@-others
#@-node:tbrown.20071007201356:class gNode
#@+node:tbrown.20071004135224.1:class tGraph
class tGraph:
    """Minimalist graph-of-tnodes wrapper"""
    #@    @+others
    #@+node:tbrown.20071004135224.2:__init__
    def __init__(self):
        self._nodes = set()
        self._edges = set()
        self._gnxStr2tnode = {}
    #@-node:tbrown.20071004135224.2:__init__
    #@+node:tbrown.20071004135224.3:nodes
    def nodes(self):
        """Return set of nodes"""
        return self._nodes
    #@-node:tbrown.20071004135224.3:nodes
    #@+node:tbrown.20071004135224.4:edges
    def edges(self):
        """Return set of (node0, node1) tuples"""
        return self._edges
    #@-node:tbrown.20071004135224.4:edges
    #@+node:tbrown.20071004135224.5:addNode
    def addNode(self, n):
        """Add n as a node"""
        self._nodes.add(n)
    #@-node:tbrown.20071004135224.5:addNode
    #@+node:tbrown.20071004135224.6:addDirectedEdge
    def addDirectedEdge(self, n0, n1):
        """Add an edge from n0 and n1 as a node"""
        self._edges.add((n0, n1))
    #@-node:tbrown.20071004135224.6:addDirectedEdge
    #@+node:tbrown.20071004141737:addGraphFromPosition
    def addGraphFromPosition(self, p, splitLabels = True):
        """
        *Add* nodes and edges from the position and its descendants.

        Need to add all the nodes before trying to resolve edges from @links
        """

        self.splitLabels = splitLabels
        if '@graph' in p.h:
            for p1 in p.children():
                self._addNodesLinks(p1)
            for p1 in p.children():
                self._addLinks(p1)
        else:
            self._addNodesLinks(p)
            self._addLinks(p)
    #@-node:tbrown.20071004141737:addGraphFromPosition
    #@+node:tbrown.20071004152905:createTreeFromGraph
    def createTreeFromGraph(self, p):
        """Build tree representing graph after p, assuming our nodes are gNodes"""

        todo = set(self.nodes())

        root = p.insertAfter()
        if '@graph' in p.h:
            root.setHeadString(p.h)
        else:
            root.setHeadString('@graph ' + p.h)
        root.expand()
        pos = root.copy()

        node2tnode = {}  # for gnx lookups for making @links

        inOut = {}  # count in and out for each node
        for n0, n1 in self.edges():
            if n0 in inOut:  # out edge
                inOut[n0] = (inOut[n0][0], inOut[n0][1]+1)
            else:
                inOut[n0] = (0,1)
            if n1 in inOut:  # in edge
                inOut[n1] = (inOut[n1][0]+1, inOut[n1][1])
            else:
                inOut[n1] = (1,0)

        def nextStart(todo):
            """find the node with the fewest in edges and most
             out edges, writing these node first may give a more
             human readable tree representation of the graph"""
            maxOut = -1
            minIn = 9999
            maxIdx = None
            for i in todo:
                In = inOut[i][0]
                out = inOut[i][1]
                if In < minIn:
                    maxOut = out
                    minIn = In
                    maxIdx = i
                if In == minIn and out > maxOut:
                    maxOut = out
                    minIn = In
                    maxIdx = i
            return maxIdx

        def makeTree(pos, node0):

            nd = pos.insertAsLastChild()
            nd.expand()
            nd.setDirty()
            self._setIndex(nd)
            node2tnode[node0] = str(nd.v.fileIndex)
            node0.writetnode(nd.v, vnode = nd.v)
            #X if hasattr(node0, 'vnode'):
            #X     if node0.vnode != None:
            #X         if not hasattr(nd.v, 'unknownAttributes'):
            #X             nd.v.unknownAttributes = {}
            #X         nd.v.unknownAttributes.update(
            #X             node0.vnode.get('unknownAttributes', {}))

            desc = [i[1] for i in self.edges() if i[0] == node0]
            while desc:
                node1 = nextStart(desc)
                desc.remove(node1)

                if node1 in todo:
                    todo.remove(node1)
                    makeTree(nd, node1)
                else:
                    lnk = nd.insertAsLastChild()
                    lnk.setHeadString(
                        self._formatLink(node2tnode[node1], node1.title))

        while todo:
            next = nextStart(todo)
            todo.remove(next)
            makeTree(pos, next)

        ans = pos

        # if only one top level node, remove the holder node
        if pos.numberOfChildren() == 1:
            ch = pos.children().next()
            ch._linkAfter(pos)
            ans = ch
            pos._unlink()

        return ans
    #@-node:tbrown.20071004152905:createTreeFromGraph
    #@+node:tbrown.20071004141737.1:_addNodesLinks
    def _addNodesLinks(self, p):
        """Add nodes and simple descendent links from p"""

        gn = gNode()
        gn.readtnode(p.v, splitLabels = self.splitLabels, vnode = p.v)
        self.addNode(gn)
        self._gnxStr2tnode[str(p.v.fileIndex)] = gn

        for nd0 in p.children():
            if nd0.h.startswith('@link'): continue
            gn1 = self._addNodesLinks(nd0)
            self.addDirectedEdge(gn, gn1)

        return gn
    #@-node:tbrown.20071004141737.1:_addNodesLinks
    #@+node:tbrown.20071004141737.2:_addLinks
    def _addLinks(self, p):
        """Collect the @links from p, now we know the nodes are in
        self._gnxStr2tnode"""

        s0 = str(p.v.fileIndex)
        for nd0 in p.children():
            if nd0.h.startswith('@link'):
                s1 = self._indexStrFromStr(nd0.h)
                try:
                    fnd = self._gnxStr2tnode[s0]
                    tnd = self._gnxStr2tnode[s1]
                    self.addDirectedEdge(fnd, tnd)
                except:  # @link node went stale
                    g.es('Broken %s' % nd0.h)
                    raise
            else:
                self._addLinks(nd0)
    #@-node:tbrown.20071004141737.2:_addLinks
    #@+node:tbrown.20071004141911:_setIndex
    def _setIndex(self, p):
        """fresh tnodes may not have .fileIndex, this adds it"""
        try:
            theId,time,n = p.v.fileIndex
        except TypeError:
            p.v.fileIndex = g.app.nodeIndices.getNewIndex()
    #@-node:tbrown.20071004141911:_setIndex
    #@+node:tbrown.20071004141931:_indexStrFromStr
    def _indexStrFromStr(self, s):
        """isolate the '(...)' part of s"""
        return s[s.find('(') : s.find(')')+1]
    #@-node:tbrown.20071004141931:_indexStrFromStr
    #@+node:tbrown.20071004155803:_formatLink
    def _formatLink(self, tid, hs):
        """format @link headString,
        strips '(' and ')' so _indexStrFromStr works"""
        return '@link %s %s' % (hs.replace('(','[').replace(')',']'),str(tid))
    #@-node:tbrown.20071004155803:_formatLink
    #@-others
#@-node:tbrown.20071004135224.1:class tGraph
#@+node:tbrown.20071004225829:class tGraphUtil
class tGraphUtil(tGraph):
    """Misc. utility functions on a tGraph"""

    #@    @+others
    #@+node:tbrown.20071004225829.1:dotStrFromPosition
    def dotStrFromPosition(self,p, includeXY = True):
        """return complete Graphviz dot format graph text"""
        self.addGraphFromPosition(p)
        node = {}  # gnx to node number map
        nodes = []
        for n,i in enumerate(self.nodes()):
            node[i]=n
            xy = ""
            if includeXY:
                xy += ' pos="%d,%d"' % (i.x, i.y)
            color = ''
            try:
                color = i.vnode.unknownAttributes['annotate']['bg']
                if color:

                    color = ' style="filled" fillcolor="%s"' % color
            except:
                color = ''
            nodes.append('n%s [label="%s"%s%s]' % (
                n, i.title.replace('\n', '\\n'), xy, color))
        edges = []
        for f,t in self.edges():
            edges.append('n%d -> n%d' % (node[f],node[t]))

        return 'digraph G {\n%s\n\n%s\n}' % ('\n'.join(nodes), '\n'.join(edges))
    #@-node:tbrown.20071004225829.1:dotStrFromPosition
    #@-others
#@-node:tbrown.20071004225829:class tGraphUtil
#@+node:ekr.20071004090250.11:class GraphEd
class GraphEd:

    '''A per-commander class that recolors outlines.'''

    #@    @+others
    #@+node:ekr.20071004090250.12:__init__
    def __init__ (self,c):

        self.dictName = 'graphed'  # for uA dictionary

        self.c = c
        table = []
        if Gato_ok: table.append(("Edit node as graph",None,self.editGraph))
        table += (
                 # BROKEN ("Edit whole tree as graph",None,self.editWholeTree),
                 ("Copy link to clipboard",None,self.copyLink),
                 ("Follow link",None,self.followLink),
                 ("Export to Graphviz dot format",None,self.dotFile),
                 ("Make Graphviz dot node",None,self.dotNode),
                 # CAN'T ("Layout using Graphviz dot",None,self.undone), # FIXME
                 )
        c.frame.menu.createNewMenu('Graph', 'Outline')
        c.frame.menu.createMenuItemsFromTable('Graph', table)
    #@-node:ekr.20071004090250.12:__init__
    #@+node:ekr.20071004090250.13:close
    def close(self, tag, key):
        "unregister handlers on closing commander"

        if self.c != key['c']: return  # not our problem

        # for i in self.handlers:
        #     pass # FIXME no handlers?

    #@-node:ekr.20071004090250.13:close
    #@+node:ekr.20071004090250.14:setIndex
    def setIndex(self, p):
        try:
            theId,time,n = p.v.fileIndex
        except TypeError:
            p.v.fileIndex = g.app.nodeIndices.getNewIndex()
    #@-node:ekr.20071004090250.14:setIndex
    #@+node:ekr.20071004090250.15:indexStrFromStr
    def indexStrFromStr(self, s):
        """isolate the '(...)' part of s"""
        return s[s.find('(') : s.find(')')+1]
    #@-node:ekr.20071004090250.15:indexStrFromStr
    #@+node:ekr.20071004090250.16:attributes...
    #@+node:ekr.20071004090250.17:getat
    def getat(self, node, attrib):

        if (not hasattr(node,'unknownAttributes') or
            not node.unknownAttributes.has_key(self.dictName) or
            not type(node.unknownAttributes[self.dictName]) == type({}) or
            not node.unknownAttributes[self.dictName].has_key(attrib)):

            return None

        return node.unknownAttributes[self.dictName][attrib]
    #@nonl
    #@-node:ekr.20071004090250.17:getat
    #@+node:ekr.20071004090250.18:setat
    def setat(self, node, attrib, val):
        "new attrbiute setter"

        #X isDefault = self.testDefault(attrib, val)

        if (not hasattr(node,'unknownAttributes') or
            not node.unknownAttributes.has_key(self.dictName) or
            type(node.unknownAttributes[self.dictName]) != type({})):
            # dictionary doesn't exist

            #X if isDefault:
            #X     return  # don't create dict. for default value

            if not hasattr(node,'unknownAttributes'):  # node has no unknownAttributes
                node.unknownAttributes = {}
                node.unknownAttributes[self.dictName] = {}
            else:  # our private dictionary isn't present
                if (not node.unknownAttributes.has_key(self.dictName) or
                    type(node.unknownAttributes[self.dictName]) != type({})):
                    node.unknownAttributes[self.dictName] = {}

            node.unknownAttributes[self.dictName][attrib] = val

            return

        # dictionary exists

        node.unknownAttributes[self.dictName][attrib] = val

        #X if isDefault:  # check if all default, if so drop dict.
        #X     self.dropEmpty(node, dictOk = True)
    #@-node:ekr.20071004090250.18:setat
    #@-node:ekr.20071004090250.16:attributes...
    #@+node:ekr.20071004090250.25:safe_del
    def safe_del(self, d, k):
        "delete a key from a dict. if present"
        if d.has_key(k): del d[k]
    #@nonl
    #@-node:ekr.20071004090250.25:safe_del
    #@+node:ekr.20071004090250.26:editGraph
    def editGraph(self, event=None, pos = None):

        c = self.c

        if pos == None:
            p = c.p
        else:
            p = pos

        self.p = p

        tgraph = tGraph()
        splitL = g.app.gui.runAskYesNoDialog(self.c,
            'Split labels with spaces?') 
        tgraph.addGraphFromPosition(p, splitLabels = (splitL == 'yes'))

        #X # make sure fileIndex is set on everything
        #X for p2 in p.self_and_subtree():
        #X     self.setIndex(p2)

        self.graph = Graph.Graph()
        # graph.simple = 0  # only blocks self loops?
        self.graph.directed = 1

        self.tnode2gnode = {}
        self.gnode2attribs = {}


        self.loadGraph(self.graph, tgraph)
        # self.loadGraphLinks(self.graph, p)

        editor = Gred.SAGraphEditor(g.app.root)
        self.editor = editor
        editor.dirty = 0
        editor.cVertexDefault = '#e8e8ff'
        editor.cEdgeDefault = '#a0a0a0'
        editor.cLabelDefault = 'black'
        editor.leoQuit = self.exiting
        editor.master.protocol("WM_DELETE_WINDOW", self.exiting)

        editor.ShowGraph(self.graph, "test")
        for vert in self.graph.Vertices():
            if hasattr(self.gnode2attribs[vert], 'vnode'):
                nd = self.gnode2attribs[vert].vnode
                if hasattr(nd, 'unknownAttributes'):
                    if 'annotate' in nd.unknownAttributes:
                        if 'bg' in nd.unknownAttributes['annotate']:
                            editor.SetVertexColor(vert,
                                nd.unknownAttributes['annotate']['bg'])

        # layout = Embedder.BFSTreeEmbedder()
        # layout.Embed(self.editor)
        # self.editor.grab_set()
        # self.editor.focus_force()
        # g.app.root.wait_window(self.editor)
    #@-node:ekr.20071004090250.26:editGraph
    #@+node:ekr.20071004090250.27:editWholeTree
    #@+at
    # 
    # This doesn't work
    # 
    # def editWholeTree(self, event=None):
    #     c = self.c
    #     g.pr(c.rootPosition().h)
    #     self.editGraph(pos = c.rootPosition())
    #@-at
    #@-node:ekr.20071004090250.27:editWholeTree
    #@+node:ekr.20071004090250.28:loadGraph
    def loadGraph(self, graph, tgraph):

        for nd in tgraph.nodes():
            vid = graph.AddVertex()
            self.tnode2gnode[nd] = vid
            self.gnode2attribs[vid] = nd
            graph.SetLabeling(vid, nd.title)
            graph.SetEmbedding(vid, nd.x, nd.y)
        for nd0, nd1 in tgraph.edges():
            graph.AddEdge(self.tnode2gnode[nd0],
                          self.tnode2gnode[nd1])
    #@-node:ekr.20071004090250.28:loadGraph
    #@+node:ekr.20071004090250.30:exiting
    def exiting(self):
        ans = g.app.gui.runAskYesNoCancelDialog(
            self.c, 'Load changes?',
            'Load changes from graph editor?')
        if ans == 'yes':
            self.saveGraph(self.p, self.graph)
        if ans in ('yes', 'no'):
            self.editor.destroy()
            self.editor.master.withdraw()  # ???
        # if ans == cancel do nothing
    #@-node:ekr.20071004090250.30:exiting
    #@+node:ekr.20071004090250.31:saveGraph
    def saveGraph(self, p, graph):

        def label(i):
            """change undefined (numeric) labels from ints to strs"""
            return str(graph.GetLabeling(i))

        c = self.c
        tgraph = tGraph()
        gnode2nottnode = {}
        for node in graph.Vertices():
            tn = gNode()
            gnode2nottnode[node] = tn
            tn.title = label(node)
            x = graph.GetEmbedding(node)
            tn.x, tn.y = x.x,x.y
            if node in self.gnode2attribs:
                tn.body = self.gnode2attribs[node].body
                tn.vnode = self.gnode2attribs[node].vnode
                tn.attr.update(self.gnode2attribs[node].attr)

            tgraph.addNode(tn)

        for node0, node1 in graph.Edges():
            tgraph.addDirectedEdge(gnode2nottnode[node0],gnode2nottnode[node1])

        newp = tgraph.createTreeFromGraph(p)

        c.setHeadString(p, 'OLD: ' + p.h)
        p.setDirty()
        c.selectPosition(p)
        c.contractNode()

        c.selectPosition(newp)
        c.setChanged(True)
        c.redraw()
    #@-node:ekr.20071004090250.31:saveGraph
    #@+node:ekr.20071004090250.32:copyLink
    def copyLink(self, event = None):
        c = self.c
        p = c.p
        self.setIndex(p)
        nn = p.insertAfter()
        nn.setHeadString(self.formatLink(p.v.fileIndex, p.h))
        c.selectPosition(nn)
        c.cutOutline()
        c.selectPosition(p)
        g.es('Link copied to clipboard')
    #@-node:ekr.20071004090250.32:copyLink
    #@+node:ekr.20071004090250.33:followLink
    def followLink(self, event = None):
        c = self.c
        s = c.p.h
        s = self.indexStrFromStr(s)
        for p in c.all_positions():
            if self.indexStrFromStr(str(p.v.fileIndex)) == s:
                c.selectPosition(p)
                break
        g.es('Not found')
    #@-node:ekr.20071004090250.33:followLink
    #@+node:ekr.20071004090250.34:formatLink
    def formatLink(self, tid, hs):
        return '@link %s %s' % (hs.replace('(','[').replace(')',']'),str(tid))
    #@-node:ekr.20071004090250.34:formatLink
    #@+node:tbrown.20071004225829.2:dotNode
    def dotNode(self, event=None):
        c = self.c
        p = c.p
        t = p.h
        tg = tGraphUtil()
        dot = tg.dotStrFromPosition(p)
        p = p.insertAfter()
        c.setHeadString(p, 'DOT FORMAT: ' + t)
        c.setBodyString(p, dot)
        c.selectPosition(p)
    #@-node:tbrown.20071004225829.2:dotNode
    #@+node:tbrown.20071005092239:dotFile
    def dotFile(self, event=None):
        c = self.c
        p = c.p
        t = p.h
        tg = tGraphUtil()
        dot = tg.dotStrFromPosition(p)
        fn = g.app.gui.runSaveFileDialog(
            '',
            'Save dot file to' ,
            [('Dot', '*.dot'), ('All', '*.*')],
            '.dot'
            )
        if not fn.lower().endswith('.dot'):
            fn += '.dot'
        file(fn, 'w').write(dot)
        g.es('Wrote %s' % fn)
    #@-node:tbrown.20071005092239:dotFile
    #@+node:ekr.20071004090250.35:undone
    def undone(self, event = None):
        g.app.gui.runAskOkDialog(self.c, 'Not implemented',
            "Sorry, that's not implemented yet.")
    #@-node:ekr.20071004090250.35:undone
    #@-others
#@nonl
#@-node:ekr.20071004090250.11:class GraphEd
#@-others
#@-node:ekr.20071004090250:@thin graphed.py
#@-leo
www.java2java.com | Contact Us
Copyright 2009 - 12 Demo Source and Support. All rights reserved.
All other trademarks are property of their respective owners.