| 1 | # -*- coding: utf-8 -*- |
|---|
| 2 | # |
|---|
| 3 | # Copyright (C) 2006-2008 Emmanuel Blot <emmanuel.blot@free.fr> |
|---|
| 4 | # All rights reserved. |
|---|
| 5 | # |
|---|
| 6 | # This software is licensed as described in the file COPYING, which |
|---|
| 7 | # you should have received as part of this distribution. The terms |
|---|
| 8 | # are also available at http://trac.edgewall.com/license.html. |
|---|
| 9 | # |
|---|
| 10 | # This software consists of voluntary contributions made by many |
|---|
| 11 | # individuals. For the exact contribution history, see the revision |
|---|
| 12 | # history and logs, available at http://projects.edgewall.com/trac/. |
|---|
| 13 | # |
|---|
| 14 | |
|---|
| 15 | import SVGdraw as SVG |
|---|
| 16 | import os |
|---|
| 17 | import md5 |
|---|
| 18 | |
|---|
| 19 | from colorsys import rgb_to_hsv, hsv_to_rgb |
|---|
| 20 | from math import sqrt |
|---|
| 21 | from random import randrange, seed |
|---|
| 22 | from revtree.api import * |
|---|
| 23 | from trac.core import * |
|---|
| 24 | from trac.web.href import Href |
|---|
| 25 | |
|---|
| 26 | __all__ = ['SvgColor', 'SvgGroup', 'SvgOperation', 'SvgRevtree'] |
|---|
| 27 | |
|---|
| 28 | UNIT = 25 |
|---|
| 29 | SQRT2=sqrt(2) |
|---|
| 30 | SQRT3=sqrt(3) |
|---|
| 31 | |
|---|
| 32 | # Debug functions to place debug circles on the SVG graph |
|---|
| 33 | debugw = [] |
|---|
| 34 | def dbgPt(x,y,c='red',d=5): |
|---|
| 35 | debugw.append(SVG.circle(x,y,d, 'white', c, '2')) |
|---|
| 36 | def dbgLn(x1,y1,x2,y2,c='red',w=3): |
|---|
| 37 | debugw.append(SVG.line(x1,y1,x2,y2,c,w)) |
|---|
| 38 | def dbgDump(svg): |
|---|
| 39 | map(svg.addElement, debugw) |
|---|
| 40 | |
|---|
| 41 | def textwidth(text): |
|---|
| 42 | # kludge, this should get the actual font parameters, etc... |
|---|
| 43 | length = text and len(text) or 0 |
|---|
| 44 | return (1+length)*(UNIT/2.5) |
|---|
| 45 | |
|---|
| 46 | class SvgColor(object): |
|---|
| 47 | """Helpers for color management (conversion, generation, ...)""" |
|---|
| 48 | |
|---|
| 49 | colormap = { 'black': (0,0,0), |
|---|
| 50 | 'white': (0xff,0xff,0xff), |
|---|
| 51 | 'darkred': (0x7f,0,0), |
|---|
| 52 | 'darkgreen': (0,0x7f,0), |
|---|
| 53 | 'darkblue': (0,0,0x7f), |
|---|
| 54 | 'red': (0xdf,0,0), |
|---|
| 55 | 'green': (0,0xdf,0), |
|---|
| 56 | 'blue': (0,0,0xdf), |
|---|
| 57 | 'gray': (0x7f,0x7f,0x7f), |
|---|
| 58 | 'orange': (0xff,0x9f,0) } |
|---|
| 59 | |
|---|
| 60 | def __init__(self, value=None, name=None): |
|---|
| 61 | if value is not None: |
|---|
| 62 | if isinstance(value, SvgColor): |
|---|
| 63 | self._color = value._color |
|---|
| 64 | elif isinstance(value, unicode): |
|---|
| 65 | self._color = SvgColor.str2col(value.encode('ascii')) |
|---|
| 66 | elif isinstance(value, str): |
|---|
| 67 | self._color = SvgColor.str2col(value) |
|---|
| 68 | elif isinstance(value, tuple): |
|---|
| 69 | if len(value) != 3: |
|---|
| 70 | raise AssertionError, "invalid color values" |
|---|
| 71 | self._color = value |
|---|
| 72 | else: |
|---|
| 73 | raise AssertionError, "unsupportedcolor: %s" % value |
|---|
| 74 | elif name is not None: |
|---|
| 75 | self._color = SvgColor.from_name(name) |
|---|
| 76 | else: |
|---|
| 77 | self._color = SvgColor.random() |
|---|
| 78 | |
|---|
| 79 | def __str__(self): |
|---|
| 80 | return "#%02x%02x%02x" % self._color |
|---|
| 81 | |
|---|
| 82 | def rgb(self): |
|---|
| 83 | return "rgb(%d,%d,%d)" % self._color |
|---|
| 84 | |
|---|
| 85 | def set(self, string): |
|---|
| 86 | self._color = SvgColor.str2col(string) |
|---|
| 87 | |
|---|
| 88 | def str2col(string): |
|---|
| 89 | if string.startswith('#'): |
|---|
| 90 | string = string[1:] |
|---|
| 91 | if len(string) == 6: |
|---|
| 92 | r = int(string[0:2], 16) |
|---|
| 93 | g = int(string[2:4], 16) |
|---|
| 94 | b = int(string[4:6], 16) |
|---|
| 95 | return (r,g,b) |
|---|
| 96 | elif len(string) == 3: |
|---|
| 97 | r = int(string[0:1], 16)*16 |
|---|
| 98 | g = int(string[1:2], 16)*16 |
|---|
| 99 | b = int(string[2:3], 16)*16 |
|---|
| 100 | return (r,g,b) |
|---|
| 101 | else: |
|---|
| 102 | raise AssertionError, "invalid color" |
|---|
| 103 | else: |
|---|
| 104 | if SvgColor.colormap.has_key(string): |
|---|
| 105 | return SvgColor.colormap[string] |
|---|
| 106 | else: |
|---|
| 107 | raise AssertionError, "unknown color: %s" % string |
|---|
| 108 | str2col = staticmethod(str2col) |
|---|
| 109 | |
|---|
| 110 | def random(): |
|---|
| 111 | rand = "%03d" % randrange(1000) |
|---|
| 112 | return (128+14*int(rand[0]), |
|---|
| 113 | 128+14*int(rand[1]), |
|---|
| 114 | 128+14*int(rand[2])) |
|---|
| 115 | random = staticmethod(random) |
|---|
| 116 | |
|---|
| 117 | def from_name(name): |
|---|
| 118 | dig = md5.new(name.encode('utf-8')).digest() |
|---|
| 119 | vr = 14*(int(ord(dig[0]))%10) |
|---|
| 120 | vg = 14*(int(ord(dig[1]))%10) |
|---|
| 121 | vb = 14*(int(ord(dig[2]))%10) |
|---|
| 122 | return (128+vr, 128+vg, 128+vb) |
|---|
| 123 | from_name = staticmethod(from_name) |
|---|
| 124 | |
|---|
| 125 | def invert(self): |
|---|
| 126 | self._color = (0xff-self._color[0], |
|---|
| 127 | 0xff-self._color[1], |
|---|
| 128 | 0xff-self._color[2]) |
|---|
| 129 | |
|---|
| 130 | def strongify(self): |
|---|
| 131 | (r,g,b) = (float(self._color[0])/0xff, |
|---|
| 132 | float(self._color[1])/0xff, |
|---|
| 133 | float(self._color[2])/0xff) |
|---|
| 134 | (h,s,v) = rgb_to_hsv(r,g,b) |
|---|
| 135 | v /= 1.5; |
|---|
| 136 | s *= 3.0; |
|---|
| 137 | if s > 1: s = 1 |
|---|
| 138 | (r,g,b) = hsv_to_rgb(h,s,v) |
|---|
| 139 | return SvgColor((int(r*0xff),int(g*0xff),int(b*0xff))) |
|---|
| 140 | |
|---|
| 141 | def lighten(self): |
|---|
| 142 | (r,g,b) = (float(self._color[0])/0xff, |
|---|
| 143 | float(self._color[1])/0xff, |
|---|
| 144 | float(self._color[2])/0xff) |
|---|
| 145 | (h,s,v) = rgb_to_hsv(r,g,b) |
|---|
| 146 | v *= 1.5; |
|---|
| 147 | if v > 1: v = 1 |
|---|
| 148 | (r,g,b) = hsv_to_rgb(h,s,v) |
|---|
| 149 | return SvgColor((int(r*0xff),int(g*0xff),int(b*0xff))) |
|---|
| 150 | |
|---|
| 151 | |
|---|
| 152 | class SvgBaseChangeset(object): |
|---|
| 153 | """Base class for graphical changeset/revision nodes |
|---|
| 154 | This changeset cannot be rendered in the SVG graph""" |
|---|
| 155 | |
|---|
| 156 | def __init__(self, parent, revision, position=None): |
|---|
| 157 | self._parent = parent |
|---|
| 158 | self._revision = revision |
|---|
| 159 | self._position = position |
|---|
| 160 | self._htw = textwidth(str(self._revision))/2 |
|---|
| 161 | self._radius = self._htw + UNIT/6 |
|---|
| 162 | self._extent = (2*self._radius,2*self._radius) |
|---|
| 163 | |
|---|
| 164 | def __cmp__(self, other): |
|---|
| 165 | return cmp(self._revision, other._revision) |
|---|
| 166 | |
|---|
| 167 | def build(self): |
|---|
| 168 | if self._position is None: |
|---|
| 169 | self._position = self._parent.get_slot(self._revision) |
|---|
| 170 | |
|---|
| 171 | def extent(self): |
|---|
| 172 | return self._extent |
|---|
| 173 | |
|---|
| 174 | def branch(self): |
|---|
| 175 | return self._parent |
|---|
| 176 | |
|---|
| 177 | def visible(self): |
|---|
| 178 | return False |
|---|
| 179 | |
|---|
| 180 | def position(self, anchor=''): |
|---|
| 181 | (x,y) = self._position |
|---|
| 182 | fo = self._radius |
|---|
| 183 | ho = SQRT2*fo/2 |
|---|
| 184 | h = len(anchor) > 1 |
|---|
| 185 | if 'n' in anchor: |
|---|
| 186 | y -= (h and ho or fo); |
|---|
| 187 | if 's' in anchor: |
|---|
| 188 | y += (h and ho or fo); |
|---|
| 189 | if 'w' in anchor: |
|---|
| 190 | x -= (h and ho or fo); |
|---|
| 191 | if 'e' in anchor: |
|---|
| 192 | x += (h and ho or fo); |
|---|
| 193 | return (x,y) |
|---|
| 194 | |
|---|
| 195 | def render(self): |
|---|
| 196 | pass |
|---|
| 197 | |
|---|
| 198 | |
|---|
| 199 | class SvgChangeset(SvgBaseChangeset): |
|---|
| 200 | """Changeset/revision node""" |
|---|
| 201 | |
|---|
| 202 | def __init__(self, parent, changeset): |
|---|
| 203 | SvgBaseChangeset.__init__(self, parent, changeset.rev) |
|---|
| 204 | self._shape = 'circle' |
|---|
| 205 | self._enhance = False |
|---|
| 206 | self._tag_offset = 0 |
|---|
| 207 | self._fillcolor = self._parent.fillcolor() |
|---|
| 208 | self._strokecolor = self._parent.strokecolor() |
|---|
| 209 | self._textcolor = SvgColor('black') |
|---|
| 210 | self._classes = ['svgchangeset'] |
|---|
| 211 | |
|---|
| 212 | def set_shape(self, shape): |
|---|
| 213 | """Define the shape of the svg changeset [circle,square,hexa]. |
|---|
| 214 | If the first letter is uppercase, the shape is augmented with |
|---|
| 215 | fancy lines. |
|---|
| 216 | """ |
|---|
| 217 | self._shape = shape.lower() |
|---|
| 218 | self._enhance = shape[0] != self._shape[0] |
|---|
| 219 | |
|---|
| 220 | def mark_first(self): |
|---|
| 221 | """Marks the changeset as the first of the branch. |
|---|
| 222 | Inverts the background and the foreground color""" |
|---|
| 223 | self._classes.append('firstchangeset') |
|---|
| 224 | |
|---|
| 225 | def mark_last(self): |
|---|
| 226 | """Mark the changeset as the latest of the branch""" |
|---|
| 227 | self._classes.append('lastchangeset') |
|---|
| 228 | |
|---|
| 229 | def build(self): |
|---|
| 230 | SvgBaseChangeset.build(self) |
|---|
| 231 | (fgc, bgc) = (self._strokecolor, self._fillcolor) |
|---|
| 232 | txc = self._textcolor |
|---|
| 233 | if 'firstchangeset' in self._classes: |
|---|
| 234 | (fgc, bgc) = (bgc, fgc) |
|---|
| 235 | if 'lastchangeset' in self._classes: |
|---|
| 236 | bgc = SvgColor('black') |
|---|
| 237 | txc = SvgColor('white') |
|---|
| 238 | |
|---|
| 239 | widgets = [] |
|---|
| 240 | if self._shape == 'circle': |
|---|
| 241 | widgets.append(SVG.circle(self._position[0], self._position[1], |
|---|
| 242 | self._radius, bgc, fgc, |
|---|
| 243 | self._parent.strokewidth())) |
|---|
| 244 | if self._enhance: |
|---|
| 245 | (x,y) = self._position |
|---|
| 246 | (d,hr) = (self._radius*SQRT3/2, self._radius/2) |
|---|
| 247 | widgets.append(SVG.line(x-d,y-hr,x+d,y-hr, |
|---|
| 248 | fgc, self._parent.strokewidth())) |
|---|
| 249 | widgets.append(SVG.line(x-d,y+hr,x+d,y+hr, |
|---|
| 250 | fgc, self._parent.strokewidth())) |
|---|
| 251 | |
|---|
| 252 | elif self._shape == 'square': |
|---|
| 253 | r = UNIT/6 |
|---|
| 254 | size = self._radius-r |
|---|
| 255 | widgets.append(SVG.rect(self._position[0]-size, |
|---|
| 256 | self._position[1]-size, |
|---|
| 257 | 2*size, 2*size, bgc, fgc, |
|---|
| 258 | self._parent.strokewidth())) |
|---|
| 259 | outline.attributes['rx'] = r |
|---|
| 260 | outline.attributes['ry'] = r |
|---|
| 261 | |
|---|
| 262 | elif self._shape == 'hexa': |
|---|
| 263 | (x,y) = self._position |
|---|
| 264 | (r,hr) = (self._radius, self._radius/2) |
|---|
| 265 | pd = SVG.pathdata() |
|---|
| 266 | pd.move(x,y-r) |
|---|
| 267 | pd.line(x+r,y-hr) |
|---|
| 268 | pd.line(x+r,y+hr) |
|---|
| 269 | pd.line(x,y+r) |
|---|
| 270 | pd.line(x-r,y+hr) |
|---|
| 271 | pd.line(x-r,y-hr) |
|---|
| 272 | pd.line(x,y-r) |
|---|
| 273 | widgets.append(SVG.path(pd, bgc, fgc, |
|---|
| 274 | self._parent.strokewidth())) |
|---|
| 275 | else: |
|---|
| 276 | raise AssertionError, \ |
|---|
| 277 | "unsupported changeset shape (%d)" % self._revision |
|---|
| 278 | title = SVG.text(self._position[0], |
|---|
| 279 | self._position[1] + UNIT/6, |
|---|
| 280 | str(self._revision), |
|---|
| 281 | self._parent.fontsize(), self._parent.fontname()) |
|---|
| 282 | title.attributes['style'] = 'fill:%s; text-anchor: middle' % txc.rgb() |
|---|
| 283 | widgets.append(title) |
|---|
| 284 | g = SVG.group('grp%d' % self._revision, elements=widgets) |
|---|
| 285 | link = "%s/changeset/%d" % (self._parent.urlbase(), self._revision) |
|---|
| 286 | self._link = SVG.link(link, elements=[g]) |
|---|
| 287 | if self._revision: |
|---|
| 288 | self._link.attributes['style'] = \ |
|---|
| 289 | 'color: %s; background-color: %s' % \ |
|---|
| 290 | (self._strokecolor, self._fillcolor) |
|---|
| 291 | self._link.attributes['id'] = 'rev%d' % self._revision |
|---|
| 292 | self._link.attributes['class'] = ' '.join(self._classes) |
|---|
| 293 | |
|---|
| 294 | def tag_offset(self, height): |
|---|
| 295 | offset = self._tag_offset |
|---|
| 296 | self._tag_offset += height |
|---|
| 297 | return offset |
|---|
| 298 | |
|---|
| 299 | def strokewidth(self): |
|---|
| 300 | return self._parent.strokewidth() |
|---|
| 301 | |
|---|
| 302 | def strokecolor(self): |
|---|
| 303 | return self._parent.strokecolor() |
|---|
| 304 | |
|---|
| 305 | def fillcolor(self): |
|---|
| 306 | return self._parent.fillcolor() |
|---|
| 307 | |
|---|
| 308 | def fontsize(self): |
|---|
| 309 | return self._parent.fontsize() |
|---|
| 310 | |
|---|
| 311 | def fontname(self): |
|---|
| 312 | return self._parent.fontname() |
|---|
| 313 | |
|---|
| 314 | def urlbase(self): |
|---|
| 315 | return self._parent.urlbase() |
|---|
| 316 | |
|---|
| 317 | def visible(self): |
|---|
| 318 | return True |
|---|
| 319 | |
|---|
| 320 | def render(self): |
|---|
| 321 | self._parent.svg().addElement(self._link) |
|---|
| 322 | |
|---|
| 323 | |
|---|
| 324 | class SvgBranchHeader(object): |
|---|
| 325 | """Branch title""" |
|---|
| 326 | |
|---|
| 327 | def __init__(self, parent, path, title, lastrev): |
|---|
| 328 | self._parent = parent |
|---|
| 329 | self._title = title or '' |
|---|
| 330 | self._path = path |
|---|
| 331 | self._rev = lastrev |
|---|
| 332 | self._tw = textwidth(self._title)+UNIT/2 |
|---|
| 333 | self._w = max(self._tw, 6*UNIT) |
|---|
| 334 | self._h = 2*UNIT |
|---|
| 335 | |
|---|
| 336 | def position(self, anchor=''): |
|---|
| 337 | (x,y) = (self._position[0]+self._w/2, self._position[1]) |
|---|
| 338 | if 'n' in anchor: |
|---|
| 339 | pass; |
|---|
| 340 | if 's' in anchor: |
|---|
| 341 | y += self._h; |
|---|
| 342 | if 'w' in anchor: |
|---|
| 343 | pass; |
|---|
| 344 | if 'e' in anchor: |
|---|
| 345 | x += self._w; |
|---|
| 346 | return (x,y) |
|---|
| 347 | |
|---|
| 348 | def extent(self): |
|---|
| 349 | return (self._w, self._h) |
|---|
| 350 | |
|---|
| 351 | def build(self): |
|---|
| 352 | self._position = self._parent.position() |
|---|
| 353 | x = self._position[0]+(self._w-self._tw)/2 |
|---|
| 354 | y = self._position[1] |
|---|
| 355 | r = UNIT/2 |
|---|
| 356 | rect = SVG.rect(x,y,self._tw,self._h, |
|---|
| 357 | self._parent.fillcolor(), |
|---|
| 358 | self._parent.strokecolor(), |
|---|
| 359 | self._parent.strokewidth()) |
|---|
| 360 | rect.attributes['rx'] = r |
|---|
| 361 | rect.attributes['ry'] = r |
|---|
| 362 | text = SVG.text(self._position[0]++self._w/2, |
|---|
| 363 | self._position[1]+self._h/2+UNIT/6, |
|---|
| 364 | self._title.encode('utf-8'), |
|---|
| 365 | self._parent.fontsize(), self._parent.fontname()) |
|---|
| 366 | text.attributes['style'] = 'text-anchor: middle' |
|---|
| 367 | name = self._title.encode('utf-8').replace('/','') |
|---|
| 368 | g = SVG.group('grp%s' % name, elements=[rect, text]) |
|---|
| 369 | href = Href(self._parent.urlbase()) |
|---|
| 370 | self._link = SVG.link(href.browser(self._path, rev='%d' % self._rev), |
|---|
| 371 | elements=[g]) |
|---|
| 372 | |
|---|
| 373 | def render(self): |
|---|
| 374 | self._parent.svg().addElement(self._link) |
|---|
| 375 | |
|---|
| 376 | |
|---|
| 377 | class SvgTag(object): |
|---|
| 378 | """Graphical view of a tag""" |
|---|
| 379 | |
|---|
| 380 | def __init__(self, parent, path, title, rev, src): |
|---|
| 381 | self._parent = parent |
|---|
| 382 | self._title = title or '' |
|---|
| 383 | self._path = path |
|---|
| 384 | self._revision = rev |
|---|
| 385 | self._srcchgset = src |
|---|
| 386 | self._tw = textwidth(self._title)+UNIT/2 |
|---|
| 387 | self._w = self._tw |
|---|
| 388 | self._h = 1.2*UNIT |
|---|
| 389 | self._opacity = 75 |
|---|
| 390 | |
|---|
| 391 | def position(self, anchor=''): |
|---|
| 392 | (x,y) = (self._position[0]+self._w/2, self._position[1]) |
|---|
| 393 | if 'n' in anchor: |
|---|
| 394 | pass; |
|---|
| 395 | if 's' in anchor: |
|---|
| 396 | y += self._h; |
|---|
| 397 | if 'w' in anchor: |
|---|
| 398 | pass; |
|---|
| 399 | if 'e' in anchor: |
|---|
| 400 | x += self._w; |
|---|
| 401 | return (x,y) |
|---|
| 402 | |
|---|
| 403 | def extent(self): |
|---|
| 404 | return (self._w, self._h) |
|---|
| 405 | |
|---|
| 406 | def build(self): |
|---|
| 407 | (sx, sy) = self._srcchgset.position() |
|---|
| 408 | h_offset = self._srcchgset.tag_offset(self._h) |
|---|
| 409 | self._position = (sx + (self._srcchgset.extent()[0])/2, |
|---|
| 410 | sy - (3*self._h)/2 + h_offset) |
|---|
| 411 | x = self._position[0]+(self._w-self._tw)/2 |
|---|
| 412 | y = self._position[1] |
|---|
| 413 | r = UNIT/2 |
|---|
| 414 | rect = SVG.rect(x,y,self._tw,self._h, |
|---|
| 415 | self._srcchgset.strokecolor(), |
|---|
| 416 | self._srcchgset.fillcolor(), |
|---|
| 417 | self._srcchgset.strokewidth()) |
|---|
| 418 | rect.attributes['rx'] = r |
|---|
| 419 | rect.attributes['ry'] = r |
|---|
| 420 | rect.attributes['opacity'] = str(self._opacity/100.0) |
|---|
| 421 | text = SVG.text(self._position[0]+self._w/2, |
|---|
| 422 | self._position[1]+self._h/2+UNIT/4, |
|---|
| 423 | "%s" % self._title.encode('utf-8'), |
|---|
| 424 | self._srcchgset.fontsize(), |
|---|
| 425 | self._srcchgset.fontname()) |
|---|
| 426 | txc = SvgColor('white') |
|---|
| 427 | text.attributes['style'] = 'fill:%s; text-anchor: middle' % txc.rgb() |
|---|
| 428 | name = self._title.encode('utf-8').replace('/','') |
|---|
| 429 | g = SVG.group('grp%d' % self._revision, elements=[rect, text]) |
|---|
| 430 | link = "%s/changeset/%d" % (self._parent.urlbase(), self._revision) |
|---|
| 431 | self._link = SVG.link(link, elements=[g]) |
|---|
| 432 | self._link.attributes['id'] = 'rev%d' % self._revision |
|---|
| 433 | self._link.attributes['style'] = \ |
|---|
| 434 | 'color: %s; background-color: %s' % \ |
|---|
| 435 | (self._srcchgset.fillcolor(), self._srcchgset.strokecolor()) |
|---|
| 436 | |
|---|
| 437 | def render(self): |
|---|
| 438 | self._parent.svg().addElement(self._link) |
|---|
| 439 | |
|---|
| 440 | |
|---|
| 441 | class SvgBranch(object): |
|---|
| 442 | """Branch (set of changesets which whose commits share a common base |
|---|
| 443 | directory)""" |
|---|
| 444 | |
|---|
| 445 | def __init__(self, parent, branch, style): |
|---|
| 446 | self._parent = parent |
|---|
| 447 | self._branch = branch |
|---|
| 448 | self._svgchangesets = {} |
|---|
| 449 | self._svgtags = {} |
|---|
| 450 | self._svgwidgets = [[] for l in IRevtreeEnhancer.ZLEVELS] |
|---|
| 451 | self._maxchgextent = [0,0] |
|---|
| 452 | self._fillcolor = self._get_color(branch.name, parent.trunks) |
|---|
| 453 | self._strokecolor = self._fillcolor.strongify() |
|---|
| 454 | self._source = branch.source() |
|---|
| 455 | try: |
|---|
| 456 | self.get_slot = self.__getattribute__('get_%s_slot' % style) |
|---|
| 457 | except AttributeError: |
|---|
| 458 | raise AssertionError, "Unsupported branch style: %s" % style |
|---|
| 459 | pw = None |
|---|
| 460 | transitions = [] |
|---|
| 461 | changesets = branch.changesets(parent.revrange); |
|---|
| 462 | changesets.sort() |
|---|
| 463 | changesets.reverse() |
|---|
| 464 | if changesets[0].last: |
|---|
| 465 | # it would require parsing the history another time to find |
|---|
| 466 | # the previous changeset when it is not in the specified range |
|---|
| 467 | lastrev = changesets[len(changesets) > 1 and 1 or 0].rev |
|---|
| 468 | else: |
|---|
| 469 | lastrev = changesets[0].rev |
|---|
| 470 | self._svgheader = \ |
|---|
| 471 | SvgBranchHeader(self, branch.name, branch.prettyname, lastrev) |
|---|
| 472 | for c in changesets: |
|---|
| 473 | svgc = SvgChangeset(self, c) |
|---|
| 474 | self._update_chg_extent(svgc.extent()) |
|---|
| 475 | if pw is None: |
|---|
| 476 | transitions.append(SvgAxis(self, self._svgheader, svgc)) |
|---|
| 477 | else: |
|---|
| 478 | transitions.append(SvgTransition(self, pw, svgc, 'gray')) |
|---|
| 479 | self._svgchangesets[c] = svgc |
|---|
| 480 | self._svgwidgets[IRevtreeEnhancer.ZMID].append(svgc) |
|---|
| 481 | pw = svgc |
|---|
| 482 | svgc = SvgBaseChangeset(self, 0) |
|---|
| 483 | self._update_chg_extent(svgc.extent()) |
|---|
| 484 | self._svgchangesets[0] = svgc |
|---|
| 485 | self._svgwidgets[IRevtreeEnhancer.ZMID].append(svgc) |
|---|
| 486 | self._svgwidgets[IRevtreeEnhancer.ZMID].extend(transitions) |
|---|
| 487 | |
|---|
| 488 | def __cmp__(self, other): |
|---|
| 489 | xs = self._position[0]+self._extent[0] |
|---|
| 490 | os = other._position[0]+other._extent[0] |
|---|
| 491 | return cmp(xs,os) |
|---|
| 492 | |
|---|
| 493 | def _update_chg_extent(self, extent): |
|---|
| 494 | if self._maxchgextent[0] < extent[0]: |
|---|
| 495 | self._maxchgextent[0] = extent[0] |
|---|
| 496 | if self._maxchgextent[1] < extent[1]: |
|---|
| 497 | self._maxchgextent[1] = extent[1] |
|---|
| 498 | |
|---|
| 499 | def _get_color(self, name, trunks): |
|---|
| 500 | """Creates a random pastel color based on the branch name |
|---|
| 501 | or returns a predefined color if the branch is a trunk""" |
|---|
| 502 | if name in trunks: |
|---|
| 503 | return SvgColor(self._parent.env.config.get('revtree', |
|---|
| 504 | 'trunkcolor', |
|---|
| 505 | '#cfcfcf')) |
|---|
| 506 | else: |
|---|
| 507 | return SvgColor(name=name) |
|---|
| 508 | |
|---|
| 509 | def create_tag(self, tag): |
|---|
| 510 | svgcs = self.svgchangeset(tag.source()) |
|---|
| 511 | self._svgwidgets[IRevtreeEnhancer.ZFORE].append(\ |
|---|
| 512 | SvgTag(self, tag.name, tag.prettyname, tag.rev, svgcs)) |
|---|
| 513 | |
|---|
| 514 | def build(self, position): |
|---|
| 515 | self._position = position |
|---|
| 516 | self._slot = self._slotgen() |
|---|
| 517 | self._svgheader.build() |
|---|
| 518 | (w, h) = self._svgheader.extent() |
|---|
| 519 | for wl in self._svgwidgets: |
|---|
| 520 | for wdgt in wl: |
|---|
| 521 | if not isinstance(wdgt, SvgTag): |
|---|
| 522 | wdgt.build() |
|---|
| 523 | h += wdgt.extent()[1] |
|---|
| 524 | else: |
|---|
| 525 | wdgt.build() |
|---|
| 526 | (tw, th) = wdgt.extent() |
|---|
| 527 | nw = tw/2 + wdgt.position()[0]-position[0] |
|---|
| 528 | if nw > w: w = nw |
|---|
| 529 | self._extent = (w, h) |
|---|
| 530 | |
|---|
| 531 | def svgarrow(self, color, head): |
|---|
| 532 | return self._parent.svgarrow(color, head) |
|---|
| 533 | |
|---|
| 534 | def header(self): |
|---|
| 535 | return self._svgheader |
|---|
| 536 | |
|---|
| 537 | def svgchangesets(self): |
|---|
| 538 | return self._svgchangesets.values() |
|---|
| 539 | |
|---|
| 540 | def svgchangeset(self, changeset): |
|---|
| 541 | if self._svgchangesets.has_key(changeset): |
|---|
| 542 | return self._svgchangesets[changeset] |
|---|
| 543 | return None |
|---|
| 544 | |
|---|
| 545 | def branch(self): |
|---|
| 546 | return self._branch |
|---|
| 547 | |
|---|
| 548 | def position(self): |
|---|
| 549 | return self._position |
|---|
| 550 | |
|---|
| 551 | def extent(self): |
|---|
| 552 | return self._extent |
|---|
| 553 | |
|---|
| 554 | def get_compact_slot(self, revision): |
|---|
| 555 | return self._slot.next() |
|---|
| 556 | |
|---|
| 557 | def get_timeline_slot(self, revision): |
|---|
| 558 | x = self.vaxis() |
|---|
| 559 | if revision != 0: |
|---|
| 560 | y = self._parent.chgoffset(revision) |
|---|
| 561 | y = (2+y)*2*self._maxchgextent[1] |
|---|
| 562 | else: |
|---|
| 563 | changesets = [] |
|---|
| 564 | for (k,v) in self._svgchangesets.items(): |
|---|
| 565 | if v._revision != 0: |
|---|
| 566 | changesets.append(k) |
|---|
| 567 | changesets.sort() |
|---|
| 568 | oldest = changesets[0] |
|---|
| 569 | y = self._svgchangesets[oldest].position()[1] |
|---|
| 570 | y += 2*self._maxchgextent[1] |
|---|
| 571 | return (x,y) |
|---|
| 572 | |
|---|
| 573 | def strokewidth(self): |
|---|
| 574 | return self._parent.strokewidth() |
|---|
| 575 | |
|---|
| 576 | def strokecolor(self): |
|---|
| 577 | return self._strokecolor |
|---|
| 578 | |
|---|
| 579 | def fillcolor(self): |
|---|
| 580 | return self._fillcolor |
|---|
| 581 | |
|---|
| 582 | def fontsize(self): |
|---|
| 583 | return self._parent.fontsize |
|---|
| 584 | |
|---|
| 585 | def fontname(self): |
|---|
| 586 | return self._parent.fontname |
|---|
| 587 | |
|---|
| 588 | def urlbase(self): |
|---|
| 589 | return self._parent.urlbase() |
|---|
| 590 | |
|---|
| 591 | def _slotgen(self): |
|---|
| 592 | x = self._position[0] + self._svgheader.extent()[0]/2 |
|---|
| 593 | y = self._position[1] + self._svgheader.extent()[1] + \ |
|---|
| 594 | 2*self._maxchgextent[1] |
|---|
| 595 | while True: |
|---|
| 596 | yield (x,y) |
|---|
| 597 | y += 2*self._maxchgextent[1] |
|---|
| 598 | |
|---|
| 599 | def vaxis(self): |
|---|
| 600 | """Return the position of the vertical axis""" |
|---|
| 601 | return self._position[0] + self._svgheader.extent()[0]/2 |
|---|
| 602 | |
|---|
| 603 | def svg(self): |
|---|
| 604 | return self._parent.svg() |
|---|
| 605 | |
|---|
| 606 | def render(self, level=None): |
|---|
| 607 | self._svgheader.render() |
|---|
| 608 | if level: |
|---|
| 609 | map(lambda w: w.render(), self._svgwidgets[level]) |
|---|
| 610 | else: |
|---|
| 611 | for wl in self._svgwidgets: |
|---|
| 612 | map(lambda w: w.render(), wl) |
|---|
| 613 | |
|---|
| 614 | |
|---|
| 615 | class SvgAxis(object): |
|---|
| 616 | """Simple graphical line between a header and the youngest |
|---|
| 617 | revision of a branch""" |
|---|
| 618 | |
|---|
| 619 | def __init__(self, parent, head, tail, color='#7f7f7f'): |
|---|
| 620 | self._parent = parent |
|---|
| 621 | self._head = head |
|---|
| 622 | self._tail = tail |
|---|
| 623 | self._color = SvgColor(color) |
|---|
| 624 | |
|---|
| 625 | def build(self): |
|---|
| 626 | sp = self._head.position('s') |
|---|
| 627 | dp = self._tail.position('n') |
|---|
| 628 | self._extent = (abs(dp[0]-sp[0]),abs(dp[1]-sp[1])) |
|---|
| 629 | self._widget = SVG.line(sp[0], sp[1], dp[0], dp[1], self._color, |
|---|
| 630 | self._parent.strokewidth()) |
|---|
| 631 | self._widget.attributes['stroke-dasharray']='4,4' |
|---|
| 632 | |
|---|
| 633 | def extent(self): |
|---|
| 634 | return self._extent |
|---|
| 635 | |
|---|
| 636 | def render(self): |
|---|
| 637 | self._parent.svg().addElement(self._widget) |
|---|
| 638 | |
|---|
| 639 | |
|---|
| 640 | class SvgTransition(object): |
|---|
| 641 | """Simple graphical line between two consecutive changesets |
|---|
| 642 | on the same branch""" |
|---|
| 643 | |
|---|
| 644 | def __init__(self, parent, srcChg, dstChg, color): |
|---|
| 645 | self._parent = parent |
|---|
| 646 | self._source = srcChg |
|---|
| 647 | self._dest = dstChg |
|---|
| 648 | self._color = color |
|---|
| 649 | |
|---|
| 650 | def build(self): |
|---|
| 651 | sp = self._dest.position('n') |
|---|
| 652 | dp = self._source.position('s') |
|---|
| 653 | self._extent = (abs(dp[0]-sp[0]),abs(dp[1]-sp[1])) |
|---|
| 654 | self._widget = SVG.line(sp[0], sp[1], dp[0], dp[1], self._color, |
|---|
| 655 | self._parent.strokewidth()) |
|---|
| 656 | self._widget.attributes['marker-end'] = \ |
|---|
| 657 | self._parent.svgarrow(self._color, False) |
|---|
| 658 | |
|---|
| 659 | def extent(self): |
|---|
| 660 | return self._extent |
|---|
| 661 | |
|---|
| 662 | def render(self): |
|---|
| 663 | self._parent.svg().addElement(self._widget) |
|---|
| 664 | |
|---|
| 665 | |
|---|
| 666 | class SvgGroup(object): |
|---|
| 667 | """Graphical group of consecutive changesets within a same branch""" |
|---|
| 668 | |
|---|
| 669 | def __init__(self, parent, firstChg, lastChg, |
|---|
| 670 | color='#fffbdb', opacity=50): |
|---|
| 671 | self._parent = parent |
|---|
| 672 | self._first = firstChg |
|---|
| 673 | self._last = lastChg |
|---|
| 674 | self._fillcolor = SvgColor(color) |
|---|
| 675 | self._strokecolor = self._fillcolor.strongify() |
|---|
| 676 | self._opacity = opacity |
|---|
| 677 | |
|---|
| 678 | def build(self): |
|---|
| 679 | spos = self._first.position()[1] |
|---|
| 680 | epos = self._last.position()[1] |
|---|
| 681 | if spos > epos: |
|---|
| 682 | (self._first, self._last) = (self._last, self._first) |
|---|
| 683 | sp = self._first.position('n') |
|---|
| 684 | ep = self._last.position('s') |
|---|
| 685 | r = UNIT/2 |
|---|
| 686 | w = self._first.extent()[0] + UNIT |
|---|
| 687 | h = ep[1] - sp[1] + UNIT |
|---|
| 688 | x = sp[0] - w/2 |
|---|
| 689 | y = sp[1] - UNIT/2 |
|---|
| 690 | self._widget = SVG.rect(x,y,w,h, |
|---|
| 691 | self._fillcolor, |
|---|
| 692 | self._strokecolor, |
|---|
| 693 | self._parent.strokewidth()) |
|---|
| 694 | self._widget.attributes['rx'] = r |
|---|
| 695 | self._widget.attributes['ry'] = r |
|---|
| 696 | self._widget.attributes['opacity'] = str(self._opacity/100.0) |
|---|
| 697 | self._extent = (w,h) |
|---|
| 698 | |
|---|
| 699 | def extent(self): |
|---|
| 700 | return self._extent |
|---|
| 701 | |
|---|
| 702 | def render(self): |
|---|
| 703 | self._parent.svg().addElement(self._widget) |
|---|
| 704 | |
|---|
| 705 | |
|---|
| 706 | class SvgOperation(object): |
|---|
| 707 | """Graphical operation between two changesets of distinct branches |
|---|
| 708 | (such as a switch/branch creation, a merge operation, ...)""" |
|---|
| 709 | |
|---|
| 710 | def __init__(self, parent, srcChg, dstChg, color='black', classes=[]): |
|---|
| 711 | self._parent = parent |
|---|
| 712 | self._source = srcChg |
|---|
| 713 | self._dest = dstChg |
|---|
| 714 | self._color = SvgColor(color) |
|---|
| 715 | self._classes = classes |
|---|
| 716 | |
|---|
| 717 | def build(self): |
|---|
| 718 | if self._source.branch() == self._dest.branch(): |
|---|
| 719 | self._widget = None |
|---|
| 720 | self._parent.env.log.warn("Invalid operation") |
|---|
| 721 | return |
|---|
| 722 | # get the position of the changeset to tie |
|---|
| 723 | (xs,ys) = self._source.position() |
|---|
| 724 | (xe,ye) = self._dest.position() |
|---|
| 725 | # swap start and end points so that xs < xe |
|---|
| 726 | if xs > xe: |
|---|
| 727 | head = True |
|---|
| 728 | (self._source, self._dest) = (self._dest, self._source) |
|---|
| 729 | (xs,ys) = self._source.position() |
|---|
| 730 | (xe,ye) = self._dest.position() |
|---|
| 731 | else: |
|---|
| 732 | head = False |
|---|
| 733 | xbranches = self._parent.xsvgbranches(self._source, self._dest) |
|---|
| 734 | # find which points on the changeset widget are used for connections |
|---|
| 735 | if xs < xe: |
|---|
| 736 | ss = 'e' |
|---|
| 737 | se = 'w' |
|---|
| 738 | else: |
|---|
| 739 | ss = 'w' |
|---|
| 740 | se = 'e' |
|---|
| 741 | ps = self._source.position(ss) |
|---|
| 742 | pe = self._dest.position(se) |
|---|
| 743 | # compute the straight line from start to end widgets |
|---|
| 744 | a = (ye-ys)/(xe-xs) |
|---|
| 745 | b = ys-(a*xs) |
|---|
| 746 | bz = [] |
|---|
| 747 | # compute the points through which the 'operation' curve should go |
|---|
| 748 | (xct,yct) = (ps[0],ps[1]) |
|---|
| 749 | points = [(xct,yct)] |
|---|
| 750 | for br in xbranches: |
|---|
| 751 | x = br.vaxis() |
|---|
| 752 | y = (a*x)+b |
|---|
| 753 | ycu = ycd = None |
|---|
| 754 | schangesets = br.svgchangesets() |
|---|
| 755 | schangesets.sort() |
|---|
| 756 | # add an invisible changeset in place of the branch header to avoid |
|---|
| 757 | # special case for the first changeset |
|---|
| 758 | hpos = br.header().position() |
|---|
| 759 | hchg = SvgBaseChangeset(br, 0, (hpos[0], hpos[1]+3*UNIT/2)) |
|---|
| 760 | schangesets.append(hchg) |
|---|
| 761 | schangesets.reverse() |
|---|
| 762 | pc = None |
|---|
| 763 | for c in schangesets: |
|---|
| 764 | # find the changesets which are right above and under the |
|---|
| 765 | # selected point, and store their vertical position |
|---|
| 766 | yc = c.position()[1] |
|---|
| 767 | if yc < y: |
|---|
| 768 | ycu = yc |
|---|
| 769 | if yc >= y: |
|---|
| 770 | ycd = yc |
|---|
| 771 | if not ycu: |
|---|
| 772 | if pc: |
|---|
| 773 | ycu = pc.position()[1] |
|---|
| 774 | elif c != schangesets[-1]: |
|---|
| 775 | ycu = schangesets[-1].position()[1] |
|---|
| 776 | break |
|---|
| 777 | pc = c |
|---|
| 778 | if not ycu or not ycd: |
|---|
| 779 | pass |
|---|
| 780 | # in this case, we need to create a virtual point (TODO) |
|---|
| 781 | else: |
|---|
| 782 | xt = x |
|---|
| 783 | yt = (ycu+ycd)/2 |
|---|
| 784 | if a != 0: |
|---|
| 785 | a2 = -1/a |
|---|
| 786 | b2 = yt - a2*xt |
|---|
| 787 | xl = (b2-b)/(a-a2) |
|---|
| 788 | yl = a2*xl + b2 |
|---|
| 789 | nx = xt-xl |
|---|
| 790 | ny = yt-yl |
|---|
| 791 | dist = sqrt(nx*nx+ny*ny) |
|---|
| 792 | radius = (3*c.extent()[1])/2 |
|---|
| 793 | add_point = dist < radius |
|---|
| 794 | else: |
|---|
| 795 | add_point = True |
|---|
| 796 | # do not insert a point if the ideal curve is far enough from |
|---|
| 797 | # an existing changeset |
|---|
| 798 | if add_point: |
|---|
| 799 | # update the vertical position for the bezier control |
|---|
| 800 | # point with the point that stands between both closest |
|---|
| 801 | # changesets |
|---|
| 802 | (xt,yt) = self._parent.fixup_point((xt,yt)) |
|---|
| 803 | points.append((xt,yt)) |
|---|
| 804 | if head: |
|---|
| 805 | points.append(pe) |
|---|
| 806 | else: |
|---|
| 807 | points.append((pe[0]-UNIT,pe[1])) |
|---|
| 808 | # now compute the qbezier curve |
|---|
| 809 | pd = SVG.pathdata() |
|---|
| 810 | pd.move(points[0][0],points[0][1]) |
|---|
| 811 | if head: |
|---|
| 812 | pd.line(points[0][0]+UNIT,points[0][1]) |
|---|
| 813 | for i in range(len(points)-1): |
|---|
| 814 | (xl,yl) = points[i] |
|---|
| 815 | (xr,yr) = points[i+1] |
|---|
| 816 | (xi,yi) = ((xl+xr)/2,(yl+yr)/2) |
|---|
| 817 | pd.qbezier(xl+2*UNIT,yl,xi,yi) |
|---|
| 818 | pd.qbezier(xr-2*UNIT,yr,xr,yr) |
|---|
| 819 | if not head: |
|---|
| 820 | pd.line(pe[0],pe[1]) |
|---|
| 821 | self._widget = SVG.path(pd, 'none', self._color, |
|---|
| 822 | self._parent.strokewidth()) |
|---|
| 823 | self._widget.attributes['marker-%s' % (head and 'start' or 'end') ] = \ |
|---|
| 824 | self._parent.svgarrow(self._color, head) |
|---|
| 825 | if self._classes: |
|---|
| 826 | self._widget.attributes['class'] = ' '.join(self._classes) |
|---|
| 827 | |
|---|
| 828 | def extent(self): |
|---|
| 829 | return self._extent |
|---|
| 830 | |
|---|
| 831 | def render(self): |
|---|
| 832 | if self._widget: |
|---|
| 833 | self._parent.svg().addElement(self._widget) |
|---|
| 834 | |
|---|
| 835 | |
|---|
| 836 | class SvgArrows(object): |
|---|
| 837 | """Arrow headers for graphical links and operations""" |
|---|
| 838 | |
|---|
| 839 | def __init__(self, parent): |
|---|
| 840 | self._parent = parent |
|---|
| 841 | self._markers = {} |
|---|
| 842 | |
|---|
| 843 | def _get_name(self, color, head): |
|---|
| 844 | fcolor = str(color) |
|---|
| 845 | if fcolor.startswith('#'): |
|---|
| 846 | fcolor = fcolor[1:] |
|---|
| 847 | return 'arrow_%s_%s' % (head and 'head' or 'tail', fcolor) |
|---|
| 848 | |
|---|
| 849 | def create(self, color, head): |
|---|
| 850 | name = self._get_name(color, head) |
|---|
| 851 | if not self._markers.has_key(name): |
|---|
| 852 | # It seems that WebKit needs some adjustements ... |
|---|
| 853 | # xos = (3.0*UNIT/100) |
|---|
| 854 | # yos = (3.0*UNIT/100) |
|---|
| 855 | # ... but Gecko does not |
|---|
| 856 | xos = 0 |
|---|
| 857 | yos = 0 |
|---|
| 858 | if head: |
|---|
| 859 | marker = SVG.marker(name, (0,0,10,8), 0, 4, UNIT/4, UNIT/4, |
|---|
| 860 | fill=SvgColor(color), orient='auto') |
|---|
| 861 | marker.addElement(SVG.polyline(((0-xos,4-yos),(10-xos,0-yos), |
|---|
| 862 | (10-xos,8-yos),(0-xos,4-yos)))) |
|---|
| 863 | else: |
|---|
| 864 | marker = SVG.marker(name, (0,0,10,8), 10, 4, UNIT/4, UNIT/4, |
|---|
| 865 | fill=SvgColor(color), orient='auto') |
|---|
| 866 | marker.addElement(SVG.polyline(((0+xos,0-yos),(0+xos,8-yos), |
|---|
| 867 | (10+xos,4-yos),(0+xos,0-yos)))) |
|---|
| 868 | self._markers[name] = marker |
|---|
| 869 | return name |
|---|
| 870 | |
|---|
| 871 | def build(self): |
|---|
| 872 | pass |
|---|
| 873 | |
|---|
| 874 | def render(self): |
|---|
| 875 | map(self._parent.svg().addElement, self._markers.values()) |
|---|
| 876 | |
|---|
| 877 | |
|---|
| 878 | class SvgRevtree(object): |
|---|
| 879 | """Main object that represents the revision tree as a SVG graph""" |
|---|
| 880 | |
|---|
| 881 | def __init__(self, env, repos, urlbase, enhancers, optimizer): |
|---|
| 882 | """Construct a new SVG revision tree""" |
|---|
| 883 | # Environment |
|---|
| 884 | self.env = env |
|---|
| 885 | # URL base of the repository |
|---|
| 886 | self.url_base = urlbase |
|---|
| 887 | # Repository instance |
|---|
| 888 | self.repos = repos |
|---|
| 889 | # Range of revision to process |
|---|
| 890 | self.revrange = None |
|---|
| 891 | # Optional enhancers |
|---|
| 892 | self.enhancers = enhancers |
|---|
| 893 | # Optimizer |
|---|
| 894 | self.optimizer = optimizer |
|---|
| 895 | # Trunk branches |
|---|
| 896 | self.trunks = self.env.config.get('revtree', 'trunks', |
|---|
| 897 | 'trunk').split(' ') |
|---|
| 898 | # FIXME: Use CSS properties instead - when browsers support them... |
|---|
| 899 | # Font name |
|---|
| 900 | self.fontname = self.env.config.get('revtree', 'fontname', 'arial') |
|---|
| 901 | # Font size |
|---|
| 902 | self.fontsize = self.env.config.get('revtree', 'fontsize', '14pt') |
|---|
| 903 | # Dictionary of branch widgets (branches as keys) |
|---|
| 904 | self._svgbranches = {} |
|---|
| 905 | # Markers |
|---|
| 906 | self._arrows = SvgArrows(self) |
|---|
| 907 | # List of inter branch operations |
|---|
| 908 | self._svgoperations = [] |
|---|
| 909 | # List of changeset groups |
|---|
| 910 | self._svggroups = [] |
|---|
| 911 | # Operation points |
|---|
| 912 | self._oppoints = {} |
|---|
| 913 | # Add-on elements (from enhancers) |
|---|
| 914 | self._addons = [] |
|---|
| 915 | # Init color generator with a predefined value |
|---|
| 916 | seed(0) |
|---|
| 917 | |
|---|
| 918 | def position(self): |
|---|
| 919 | """Return the position of the revision tree widget""" |
|---|
| 920 | return (UNIT,2*UNIT) |
|---|
| 921 | |
|---|
| 922 | def extent(self): |
|---|
| 923 | """Return the extent of the revtree""" |
|---|
| 924 | return (int(self._extent[0]),int(self._extent[1])) |
|---|
| 925 | |
|---|
| 926 | def strokewidth(self): |
|---|
| 927 | """Return the width of a stroke""" |
|---|
| 928 | return 3 |
|---|
| 929 | |
|---|
| 930 | def svgbranch(self, rev=None, branchname=None, branch=None): |
|---|
| 931 | """Return a branch widget, based on the revision number or the |
|---|
| 932 | branch id""" |
|---|
| 933 | if not branch: |
|---|
| 934 | if rev: |
|---|
| 935 | chg = self.repos.changeset(rev) |
|---|
| 936 | if not chg: |
|---|
| 937 | self.env.log.warn("No changeset %d" % rev) |
|---|
| 938 | return None |
|---|
| 939 | self.env.log.info("Changeset %d, branch %s" % (rev, chg.branchname)) |
|---|
| 940 | branch = self.repos.branch(chg.branchname) |
|---|
| 941 | elif branchname: |
|---|
| 942 | branch = self.repos.branch(branchname) |
|---|
| 943 | if not branch: |
|---|
| 944 | return None |
|---|
| 945 | if not self._svgbranches.has_key(branch): |
|---|
| 946 | return None |
|---|
| 947 | return self._svgbranches[branch] |
|---|
| 948 | |
|---|
| 949 | def svgbranches(self): |
|---|
| 950 | return self._svgbranches |
|---|
| 951 | |
|---|
| 952 | def svgarrow(self, color, head): |
|---|
| 953 | return 'url(#%s)' % self._arrows.create(color, head) |
|---|
| 954 | |
|---|
| 955 | def create(self, req, revisions=None, branches=None, authors=None, |
|---|
| 956 | hidetermbranch=False, style='compact'): |
|---|
| 957 | if revisions is not None: |
|---|
| 958 | self.revrange = revisions |
|---|
| 959 | else: |
|---|
| 960 | self.revrange = self.repos.revision_range() |
|---|
| 961 | if hidetermbranch: |
|---|
| 962 | allbranches = filter(lambda b: b.is_active(self.revrange), |
|---|
| 963 | self.repos.branches().values()) |
|---|
| 964 | else: |
|---|
| 965 | allbranches = self.repos.branches().values() |
|---|
| 966 | revisions = [] |
|---|
| 967 | for b in allbranches: |
|---|
| 968 | if branches: |
|---|
| 969 | if b.name not in branches: |
|---|
| 970 | continue |
|---|
| 971 | if authors: |
|---|
| 972 | if not [a for a in authors for x in b.authors() if a == x]: |
|---|
| 973 | continue |
|---|
| 974 | svgbranch = SvgBranch(self, b, style) |
|---|
| 975 | self._svgbranches[b] = svgbranch |
|---|
| 976 | revisions.extend([c.rev for c in b.changesets()]) |
|---|
| 977 | revisions.sort() |
|---|
| 978 | revisions.reverse() |
|---|
| 979 | self._vtimes = {} |
|---|
| 980 | vtime = 0 |
|---|
| 981 | for r in revisions: |
|---|
| 982 | self._vtimes[r] = vtime |
|---|
| 983 | vtime += 1 |
|---|
| 984 | for enhancer in self.enhancers: |
|---|
| 985 | self._addons.append(enhancer.create(self.env, req, |
|---|
| 986 | self.repos, self)) |
|---|
| 987 | for tag in self.repos.tags().values(): |
|---|
| 988 | self.env.log.info("Found tag: %r" % tag.name) |
|---|
| 989 | if tag.clone: |
|---|
| 990 | svgbr = self.svgbranch(rev=tag.clone[0]) |
|---|
| 991 | if svgbr: |
|---|
| 992 | svgbr.create_tag(tag) |
|---|
| 993 | |
|---|
| 994 | def build(self): |
|---|
| 995 | """Build the graph""" |
|---|
| 996 | branches = self.optimizer.optimize(self.repos, \ |
|---|
| 997 | [svgbr.branch() for svgbr in self._svgbranches.values()]) |
|---|
| 998 | branch_xpos = UNIT |
|---|
| 999 | svgbranches = [self.svgbranch(branch=b) for b in branches] |
|---|
| 1000 | for svgbranch in svgbranches: |
|---|
| 1001 | svgbranch.build((branch_xpos, UNIT/6)) |
|---|
| 1002 | #branch_xpos += svgbranch.header().extent()[0] + UNIT |
|---|
| 1003 | branch_xpos += svgbranch.extent()[0] + UNIT |
|---|
| 1004 | # TODO: discard tags for which source changeset do not exist |
|---|
| 1005 | #for svgtag in self.svgtags().values(): |
|---|
| 1006 | # self.env.log.info("Build Tag %s" % svgtag) |
|---|
| 1007 | # svgtag.build() |
|---|
| 1008 | map(lambda e: e.build(), self._addons) |
|---|
| 1009 | # FIXME: why not using svgbranches ? |
|---|
| 1010 | svgbranches = self._svgbranches.values() |
|---|
| 1011 | svgbranches.sort() |
|---|
| 1012 | maxheight = 0 |
|---|
| 1013 | for b in svgbranches: |
|---|
| 1014 | h = b.extent()[1] |
|---|
| 1015 | if h > maxheight: |
|---|
| 1016 | maxheight = h |
|---|
| 1017 | if not svgbranches: |
|---|
| 1018 | raise EmptyRangeError |
|---|
| 1019 | w = svgbranches[-1].position()[0] - svgbranches[0].position()[0] + \ |
|---|
| 1020 | svgbranches[-1].extent()[0] + 2*UNIT |
|---|
| 1021 | maxheight += UNIT |
|---|
| 1022 | self._extent = (w,maxheight) |
|---|
| 1023 | |
|---|
| 1024 | def urlbase(self): |
|---|
| 1025 | return self.url_base |
|---|
| 1026 | |
|---|
| 1027 | def chgoffset(self, revision): |
|---|
| 1028 | return self._vtimes[revision] |
|---|
| 1029 | |
|---|
| 1030 | def xsvgbranches(self, c1, c2): |
|---|
| 1031 | """Provide the ordered list of branch widgets which are |
|---|
| 1032 | between two changeset wdigets""" |
|---|
| 1033 | a1 = c1.branch().vaxis() |
|---|
| 1034 | a2 = c2.branch().vaxis() |
|---|
| 1035 | branches = filter(lambda b,l=a1,r=a2: l<b.vaxis()<r, |
|---|
| 1036 | self._svgbranches.values()) |
|---|
| 1037 | branches.sort() |
|---|
| 1038 | return branches |
|---|
| 1039 | |
|---|
| 1040 | def fixup_point(self, point): |
|---|
| 1041 | """Avoid two operation path to go through the same point |
|---|
| 1042 | Store every points of an operation path. If the point is already |
|---|
| 1043 | marked as used, find another point, looking around the original |
|---|
| 1044 | point for a free slot""" |
|---|
| 1045 | (x, y) = point |
|---|
| 1046 | kx = int(x) |
|---|
| 1047 | if self._oppoints.has_key(kx): |
|---|
| 1048 | val = 1 |
|---|
| 1049 | inc = 1 |
|---|
| 1050 | while y in self._oppoints[kx]: |
|---|
| 1051 | y += val*(UNIT/3) |
|---|
| 1052 | val = -val + inc |
|---|
| 1053 | inc = -inc |
|---|
| 1054 | else: |
|---|
| 1055 | self._oppoints[kx] = [] |
|---|
| 1056 | self._oppoints[kx].append(y) |
|---|
| 1057 | return (x, y) |
|---|
| 1058 | |
|---|
| 1059 | def svg(self): |
|---|
| 1060 | return self._svg |
|---|
| 1061 | |
|---|
| 1062 | def __str__(self): |
|---|
| 1063 | """Dump the revision tree as a SVG UTF-8 string""" |
|---|
| 1064 | import cStringIO |
|---|
| 1065 | xml=cStringIO.StringIO() |
|---|
| 1066 | self._svg.toXml(0, xml) |
|---|
| 1067 | return xml.getvalue() |
|---|
| 1068 | |
|---|
| 1069 | def save(self, filename): |
|---|
| 1070 | """Save the revision tree in a file""" |
|---|
| 1071 | d = SVG.drawing() |
|---|
| 1072 | d.setSVG(self._svg) |
|---|
| 1073 | d.toXml(filename) |
|---|
| 1074 | |
|---|
| 1075 | def render(self, scale=1.0): |
|---|
| 1076 | """Render the revision tree""" |
|---|
| 1077 | self._svg = SVG.svg((0, 0, self._extent[0], self._extent[1]), |
|---|
| 1078 | scale*self._extent[0], scale*self._extent[1], |
|---|
| 1079 | True, id='svgbox') |
|---|
| 1080 | self._arrows.render() |
|---|
| 1081 | map(lambda e: e.render(IRevtreeEnhancer.ZBACK), self._addons) |
|---|
| 1082 | map(lambda b: b.render(IRevtreeEnhancer.ZBACK), |
|---|
| 1083 | self._svgbranches.values()) |
|---|
| 1084 | map(lambda e: e.render(IRevtreeEnhancer.ZMID), self._addons) |
|---|
| 1085 | map(lambda b: b.render(IRevtreeEnhancer.ZMID), |
|---|
| 1086 | self._svgbranches.values()) |
|---|
| 1087 | map(lambda e: e.render(IRevtreeEnhancer.ZFORE), self._addons) |
|---|
| 1088 | map(lambda b: b.render(IRevtreeEnhancer.ZFORE), |
|---|
| 1089 | self._svgbranches.values()) |
|---|
| 1090 | dbgDump(self._svg) |
|---|