""" MoinMoin - VisualSiteMap action Idea is based on the webdot.py action. More or less redid it from scratch. Differs from the webdot action in several ways: * Uses the dot executable, not webdot, since webdot's not available on windows. * All links up to the search depth are displayed. * There's no maximal limit to the displayed nodes. * Nodes are marked during depth first visit, so each node is visited only once. * The visit method in class LocalSiteMap gets the whole tree as parameter. That way additional treenode information may be shown in the graph. * All edges between nodes contained in the graph are displayed, even if MAX_DEPTH is exceeded that way. * Optional depth controls * Nodes linked more then STRONG_LINK_NR times are highlighted using the STRONG_COLOR * Search depth is configurable Add this to your stylesheet: img.sitemap { border-width: 1; border-color: #000000; } 07.10.2004 * Maximum image size can be configured * Output image format is configurable * David Linke changed the output code (print() -> request.write()) * Changed link counting algorithm to get the depth controls right. 08.10.2004 * IE caching problem with depth controls resolved. Now the current search depth is part of the file names. * Problems with pagenames containing non ASCII characters fixed. $Id$ """ ################################################################## # Be warned that calculating large graphs may block your server! # # So be careful with the parameter settings. # ################################################################## # Imports import string,sys,re,os from MoinMoin import config, wikiutil, user from MoinMoin.Page import Page # Graph controls. DEFAULT_DEPTH = 2 STRONG_LINK_NR = 4 # Optional controls for interactive modification of the search depth. DEPTH_CONTROL = 0 MAX_DEPTH = 4 # This should be a public path on your web server. The dot files, images and map files are created in this directory and # served from there. CACHE_DIR = "C:/DocumentRoot/cache/"; CACHE_URL = "http://my-server/cache/"; # Absolute location of the dot (or neato) executable. DOT_EXE = "C:/Programme/ATT/GraphViz/bin/dot.exe"; # Desired image format (eg. png, jpg, gif - see the dot documentation) OUTPUT_FORMAT = "png" # Maximum output size in inches. Set to None to disable size limitation. # OUTPUT_SIZE="8,4" sets maximum width to 8, maximum height to 4 inches. OUTPUT_SIZE="10,10" # Colors of boxes and edges. BOX_COLOR ="#E0F0FF" ROOT_COLOR = "#FFE0E0" STRONG_COLOR = "#E0FFE0" EDGE_COLOR ="#888888" # Categories are filtered in some way. CATEGORY_STRING = "^Kategorie" # Code starts here def execute(pagename, request): _ = request.getText maxdepth = int(DEFAULT_DEPTH) if DEPTH_CONTROL and request.form.has_key('depth'): maxdepth = int(request.form['depth'][0]) if int(maxdepth) > int(MAX_DEPTH): maxdepth = MAX_DEPTH request.http_headers() wikiutil.send_title(request, _('Visual Map of %s') % (pagename), pagename=pagename) baseurl = request.getBaseURL() wikiname = wikiutil.quoteWikiname(pagename) dotfilename = '%s/%s_%s.dot' % (CACHE_DIR, wikiname, maxdepth) imagefilename = '%s/%s_%s.%s' % (CACHE_DIR, wikiname, maxdepth, OUTPUT_FORMAT) imageurl = '%s/%s_%s.%s' % (CACHE_URL, wikiname, maxdepth, OUTPUT_FORMAT) mapfilename = '%s/%s_%s.cmap' % (CACHE_DIR, wikiname, maxdepth) dotfile = open(dotfilename,'w') dotfile.write('digraph G {\n') if OUTPUT_SIZE: dotfile.write(' size="%s"\n' % OUTPUT_SIZE) dotfile.write(' ratio=compress;\n') dotfile.write(' URL="%s";\n' % wikiname) dotfile.write(' overlap=false;\n') dotfile.write(' concentrate=true;\n') dotfile.write(' edge [color="%s"];\n' % EDGE_COLOR) dotfile.write(' node [URL="%s/\N", ' % baseurl) dotfile.write('fontcolor=black, fontname=%s , fontsize=%s, style=filled, color="%s"]\n' % ("arial","8", BOX_COLOR)) dotfile.write(LocalSiteMap(pagename, maxdepth).output(request)) dotfile.write('}\n') dotfile.close() os.system('%s -T%s -o%s %s' % (DOT_EXE, OUTPUT_FORMAT, imagefilename, dotfilename)) os.system('%s -Tcmap -o%s %s' % (DOT_EXE, mapfilename, dotfilename)) request.write('
' % (imageurl)) request.write('') mapfile = open(mapfilename,'r') for row in mapfile.readlines(): request.write(row) mapfile.close() request.write('') if DEPTH_CONTROL: linkname = wikiutil.quoteWikiname(pagename) request.write('

') if maxdepth > 1: request.write('Less' % (baseurl, linkname, maxdepth-1)) else: request.write('Less') request.write(' | ') if maxdepth < MAX_DEPTH: request.write('More' % (baseurl, linkname, maxdepth+1)) else: request.write('More') request.write('

') request.write('

Search depth is %s. Nodes linked more than %s times are highlighted.

' % (maxdepth, STRONG_LINK_NR)) wikiutil.send_footer(request, pagename) class LocalSiteMap: def __init__(self, name, maxdepth): self.name = name self.result = [] self.maxdepth = maxdepth def output(self, request): pagebuilder = GraphBuilder(request, self.maxdepth) root = pagebuilder.build_graph(self.name) # count links # print '

All links

' for edge in pagebuilder.all_edges: edge[0].linkedfrom += 1 edge[1].linkedto += 1 # print edge[0].name + '->' + edge[1].name + '
' # write nodes for node in pagebuilder.all_nodes: self.append(' %s'% wikiutil.quoteWikiname(node.name)) if node.depth > 0: if node.linkedto >= STRONG_LINK_NR: self.append(' [label="%s",color="%s"];\n' % (node.name, STRONG_COLOR)) else: self.append(' [label="%s"];\n' % (node.name)) else: self.append('[label="%s",shape=box,style=filled,color="%s"];\n' % (node.name, ROOT_COLOR)) # write edges for edge in pagebuilder.all_edges: self.append(' %s->%s;\n' % (wikiutil.quoteWikiname(edge[0].name),wikiutil.quoteWikiname(edge[1].name))) return string.join(self.result, '') def append(self, text): self.result.append(text) class GraphBuilder: def __init__(self, request, maxdepth): self.request = request self.maxdepth = maxdepth self.all_nodes = [] self.all_edges = [] def is_ok(self, child): if not self.request.user.may.read(child): return 0 if Page(child).exists() and (not re.search(r'%s' % CATEGORY_STRING,child)): return 1 return 0 def build_graph(self, name): # Reuse generated trees nodesMap = {} root = Node(name) nodesMap[name] = root root.visited = 1 self.all_nodes.append(root) self.recurse_build([root], 1, nodesMap) return root def recurse_build(self, nodes, depth, nodesMap): # collect all nodes of the current search depth here for the next recursion step child_nodes = [] # iterate over the nodes for node in nodes: # print "

%s: Kids of %s

" % (depth,node.name) for child in Page(node.name).getPageLinks(self.request): if self.is_ok(child): # print "Child %s" % child # Create the node with the given name if not nodesMap.has_key(child): # create the new node and store it newNode = Node(child) newNode.depth = depth # print "is new" else: newNode = nodesMap[child] # print "is old" # print ".
" # If the current depth doesn't exceed the maximum depth, add newNode to recursion step if (int(depth) <= int(self.maxdepth)): # The node is appended to the nodes list for the next recursion step. nodesMap[child] = newNode self.all_nodes.append(newNode) child_nodes.append(newNode) node.append(newNode) # Draw an edge. edge = (node, newNode) if (not edge in self.all_edges): self.all_edges.append(edge) # recurse, if the current recursion step yields children if len(child_nodes): self.recurse_build(child_nodes, depth+1, nodesMap) class Node: def __init__(self, name): self.name = name self.children = [] self.visited = 0 self.linkedfrom = 0 self.linkedto = 0 self.depth = 0 def append(self, node): self.children.append(node)