~abentley/bzrtools/bzrtools.dev

« back to all changes in this revision

Viewing changes to dotgraph.py

  • Committer: Aaron Bentley
  • Date: 2005-06-08 19:00:33 UTC
  • Revision ID: abentley@panoramicfeedback.com-20050608190033-daa1703096158164
grammar

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2004, 2005 Aaron Bentley
2
 
# <aaron.bentley@utoronto.ca>
3
 
#
4
 
#    This program is free software; you can redistribute it and/or modify
5
 
#    it under the terms of the GNU General Public License as published by
6
 
#    the Free Software Foundation; either version 2 of the License, or
7
 
#    (at your option) any later version.
8
 
#
9
 
#    This program is distributed in the hope that it will be useful,
10
 
#    but WITHOUT ANY WARRANTY; without even the implied warranty of
11
 
#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
 
#    GNU General Public License for more details.
13
 
#
14
 
#    You should have received a copy of the GNU General Public License
15
 
#    along with this program; if not, write to the Free Software
16
 
#    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
17
 
 
18
 
from subprocess import Popen, PIPE
19
 
from urllib import urlencode
20
 
from xml.sax.saxutils import escape
21
 
import os.path
22
 
import errno
23
 
import tempfile
24
 
import shutil
25
 
import time
26
 
 
27
 
RSVG_OUTPUT_TYPES = ('png', 'jpg')
28
 
DOT_OUTPUT_TYPES = ('svg', 'svgz', 'gif', 'jpg', 'ps', 'fig', 'mif', 'png', 
29
 
                    'cmapx')
30
 
 
31
 
class NoDot(Exception):
32
 
    def __init__(self):
33
 
        Exception.__init__(self, "Can't find dot!")
34
 
 
35
 
class NoRsvg(Exception):
36
 
    def __init__(self):
37
 
        Exception.__init__(self, "Can't find rsvg!")
38
 
 
39
 
class Node(object):
40
 
    def __init__(self, name, color=None, label=None, rev_id=None,
41
 
                 cluster=None, node_style=None, date=None, message=None):
42
 
        self.name = name
43
 
        self.color = color
44
 
        self.label = label
45
 
        self.committer = None
46
 
        self.rev_id = rev_id
47
 
        if node_style is None:
48
 
            self.node_style = []
49
 
        self.cluster = cluster
50
 
        self.rank = None
51
 
        self.date = date
52
 
        self.message = message
53
 
        self.href = None
54
 
 
55
 
    def define(self):
56
 
        attributes = []
57
 
        style = []
58
 
        if self.color is not None:
59
 
            attributes.append('fillcolor="%s"' % self.color)
60
 
            style.append('filled')
61
 
        style.extend(self.node_style)
62
 
        if len(style) > 0:
63
 
            attributes.append('style="%s"' % ",".join(style))
64
 
        label = self.label
65
 
        if label is not None:
66
 
            attributes.append('label="%s"' % label)
67
 
        attributes.append('shape="box"')
68
 
        tooltip = ''
69
 
        if self.message is not None:
70
 
            tooltip += self.message.replace('"', '\\"')
71
 
        if tooltip:
72
 
            attributes.append('tooltip="%s"' % tooltip)
73
 
        if self.href is not None:
74
 
            attributes.append('href="%s"' % self.href)
75
 
        elif tooltip:
76
 
            attributes.append('href="#"')
77
 
        if len(attributes) > 0:
78
 
            return '%s[%s]' % (self.name, " ".join(attributes))
79
 
 
80
 
    def __str__(self):
81
 
        return self.name
82
 
 
83
 
class Edge(object):
84
 
    def __init__(self, start, end, label=None):
85
 
        object.__init__(self)
86
 
        self.start = start
87
 
        self.end = end
88
 
        self.label = label
89
 
 
90
 
    def dot(self, do_weight=False):
91
 
        attributes = []
92
 
        if self.label is not None:
93
 
            attributes.append(('label', self.label))
94
 
        if do_weight:
95
 
            weight = '0'
96
 
            if self.start.cluster == self.end.cluster:
97
 
                weight = '1'
98
 
            elif self.start.rank is None:
99
 
                weight = '1'
100
 
            elif self.end.rank is None:
101
 
                weight = '1'
102
 
            attributes.append(('weight', weight))
103
 
        if len(attributes) > 0:
104
 
            atlist = []
105
 
            for key, value in attributes:
106
 
                atlist.append("%s=\"%s\"" % (key, value))
107
 
            pq = ' '.join(atlist)
108
 
            op = "[%s]" % pq
109
 
        else:
110
 
            op = ""
111
 
        return "%s->%s%s;" % (self.start.name, self.end.name, op)
112
 
 
113
 
def make_edge(relation):
114
 
    if hasattr(relation, 'start') and hasattr(relation, 'end'):
115
 
        return relation
116
 
    return Edge(relation[0], relation[1])
117
 
 
118
 
def dot_output(relations, ranking="forced"):
119
 
    defined = {}
120
 
    yield "digraph G\n"
121
 
    yield "{\n"
122
 
    clusters = set()
123
 
    edges = [make_edge(f) for f in relations]
124
 
    def rel_appropriate(start, end, cluster):
125
 
        if cluster is None:
126
 
            return (start.cluster is None and end.cluster is None) or \
127
 
                start.cluster != end.cluster
128
 
        else:
129
 
            return start.cluster==cluster and end.cluster==cluster
130
 
 
131
 
    for edge in edges:
132
 
        if edge.start.cluster is not None:
133
 
            clusters.add(edge.start.cluster)
134
 
        if edge.end.cluster is not None:
135
 
            clusters.add(edge.end.cluster)
136
 
    clusters = list(clusters)
137
 
    clusters.append(None)
138
 
    for index, cluster in enumerate(clusters):
139
 
        if cluster is not None and ranking == "cluster":
140
 
            yield "subgraph cluster_%s\n" % index
141
 
            yield "{\n"
142
 
            yield '    label="%s"\n' % cluster
143
 
        for edge in edges:
144
 
            if edge.start.name not in defined and edge.start.cluster == cluster:
145
 
                defined[edge.start.name] = edge.start
146
 
                my_def = edge.start.define()
147
 
                if my_def is not None:
148
 
                    yield "    %s\n" % my_def
149
 
            if edge.end.name not in defined and edge.end.cluster == cluster:
150
 
                defined[edge.end.name] = edge.end
151
 
                my_def = edge.end.define()
152
 
                if my_def is not None:
153
 
                    yield "    %s;\n" % my_def
154
 
            if rel_appropriate(edge.start, edge.end, cluster):
155
 
                yield "    %s\n" % edge.dot(do_weight=ranking=="forced")
156
 
        if cluster is not None and ranking == "cluster":
157
 
            yield "}\n"
158
 
 
159
 
    if ranking == "forced":
160
 
        ranks = {}
161
 
        for node in defined.itervalues():
162
 
            if node.rank not in ranks:
163
 
                ranks[node.rank] = set()
164
 
            ranks[node.rank].add(node.name)
165
 
        sorted_ranks = [n for n in ranks.iteritems()]
166
 
        sorted_ranks.sort()
167
 
        last_rank = None
168
 
        for rank, nodes in sorted_ranks:
169
 
            if rank is None:
170
 
                continue
171
 
            yield 'rank%d[style="invis"];\n' % rank
172
 
            if last_rank is not None:
173
 
                yield 'rank%d -> rank%d[style="invis"];\n' % (last_rank, rank)
174
 
            last_rank = rank
175
 
        for rank, nodes in ranks.iteritems():
176
 
            if rank is None:
177
 
                continue
178
 
            node_text = "; ".join('"%s"' % n for n in nodes)
179
 
            yield ' {rank = same; "rank%d"; %s}\n' % (rank, node_text)
180
 
    yield "}\n"
181
 
 
182
 
def invoke_dot_aa(input, out_file, file_type='png'):
183
 
    """\
184
 
    Produce antialiased Dot output, invoking rsvg on an intermediate file.
185
 
    rsvg only supports png, jpeg and .ico files."""
186
 
    tempdir = tempfile.mkdtemp()
187
 
    try:
188
 
        temp_file = os.path.join(tempdir, 'temp.svg')
189
 
        invoke_dot(input, temp_file, 'svg')
190
 
        cmdline = ['rsvg', temp_file, out_file]
191
 
        try:
192
 
            rsvg_proc = Popen(cmdline)
193
 
        except OSError, e:
194
 
            if e.errno == errno.ENOENT:
195
 
                raise NoRsvg()
196
 
        status = rsvg_proc.wait()
197
 
    finally:
198
 
        shutil.rmtree(tempdir)
199
 
    return status
200
 
 
201
 
def invoke_dot(input, out_file=None, file_type='svg', antialias=None, 
202
 
               fontname="Helvetica", fontsize=11):
203
 
    cmdline = ['dot', '-T%s' % file_type, '-Nfontname=%s' % fontname, 
204
 
               '-Efontname=%s' % fontname, '-Nfontsize=%d' % fontsize,
205
 
               '-Efontsize=%d' % fontsize]
206
 
    if out_file is not None:
207
 
        cmdline.extend(('-o', out_file))
208
 
    try:
209
 
        dot_proc = Popen(cmdline, stdin=PIPE)
210
 
    except OSError, e:
211
 
        if e.errno == errno.ENOENT:
212
 
            raise NoDot()
213
 
        else:
214
 
            raise
215
 
    for line in input:
216
 
        dot_proc.stdin.write(line.encode('utf-8'))
217
 
    dot_proc.stdin.close()
218
 
    return dot_proc.wait()
219
 
 
220
 
def invoke_dot_html(input, out_file):
221
 
    """\
222
 
    Produce an html file, which uses a .png file, and a cmap to provide
223
 
    annotated revisions.
224
 
    """
225
 
    tempdir = tempfile.mkdtemp()
226
 
    try:
227
 
        temp_dot = os.path.join(tempdir, 'temp.dot')
228
 
        status = invoke_dot(input, temp_dot, file_type='dot')
229
 
 
230
 
        dot = open(temp_dot)
231
 
        temp_file = os.path.join(tempdir, 'temp.cmapx')
232
 
        status = invoke_dot(dot, temp_file, 'cmapx')
233
 
 
234
 
        png_file = '.'.join(out_file.split('.')[:-1] + ['png'])
235
 
        dot.seek(0)
236
 
        status = invoke_dot(dot, png_file, 'png')
237
 
 
238
 
        png_relative = png_file.split('/')[-1]
239
 
        html = open(out_file, 'wb')
240
 
        w = html.write
241
 
        w('<html><head><title></title></head>\n')
242
 
        w('<body>\n')
243
 
        w('<img src="%s" usemap="#G" border=0/>' % png_relative)
244
 
        w(open(temp_file).read())
245
 
        w('</body></html>\n')
246
 
    finally:
247
 
        shutil.rmtree(tempdir)
248
 
    return status
249