| UML-Sequence documentation | Contained in the UML-Sequence distribution. |
UML::Sequence::Svg - converts xml sequence files to svg
use UML::Sequence::Svg;
seq2svg @ARGV;
This module supports the seq2svg.pl script like Pod::Html supports pod2html. The array passed to seq2svg.pl should have the following form:
([ I<options-pairs>, ] [input_file_name])
where options-pairs are any of
-a colorspecifies a color to be used to fill the activation boxes. Must be in a form acceptable to SVG.
-c colorspecifies a color to be used to fill the class boxes. Must be in a form acceptable to SVG.
-especifies that embedded annotations are to be applied
to any rasterized version of the SVG image. Only valid when the -m or -M
option is also specified. When an arrow label has associated annotations,
the labels with be suffixed with a superscript number linking to
a text section containing the annotation text.
-g arrow-gapIf arrow-gap is an integer value, specifies number of pixels between arrows
(default is 40 pixels).
If arrow-gap is a fractional value, specifies a scaling factor for the
default number of pixels between arrows.
-jspecifies that Javascript'ed tooltip annotations are to be applied
to any rasterized version of the SVG image. Only valid when the -m or -M
option is also specified. When an arrow label has associated annotations,
the hyperlinks in the areamap for the label will include
onmouseover() function calls containing the annotation text for use with the
Javascript tooltip package available at
http://www.walterzorn.com/tooltip/tooltip_e.htm.
-m areamap-path, -M areamap-pathspecifies the name of a file to receive HTML containing an image element,
areamap, and (optionally) either an ordered list of annotations (if -e was
specified) or a script tag linking to the Javascript tooltip script (if -j
was specified). to be applied to any rasterized version of the SVG image.
Only valid when either the -p or -P option is specified.
-M specifies append mode for the output file.
-o output_file_namespecifies the output file name.
-p classdocs-path, -P classdocs-pathspecifies a base path to classdocs generated by psichedoc. -p causes
hyperlinks to the documents for individual classes and/or methods to be
embedded in the SVG file for both the class labels and method labels, excluding
method labels w/ embedded whitespace. Additionally, the specified path is used
with any generated HTML imagemap. -P is the same, except it does not
embed hyperlinks in the SVG (due to Batik's current inability to handle
rasterization of SVG's with embeded hyperlinks). In other words, use -P
to specify a path for HTML imagemaps when the SVG output will be further
processed by Batik.
-w box-widthspecifies width of class box in pixels; default is 125. Used to compute class header boxes and areamap coordinates.
-x char-widthspecifies width of characters in pixels; default is 6. Used to compute class header boxes and areamap coordinates.
-y char-heightspecifies height of characters in pixels; default is 14. Used to compute class header boxes and areamap coordinates.
By default input is from standard in and output is to standard out.
Phil Crow, <philcrow2000@yahoo.com>
Version 0.02 Updates by Dean Arnold, <darnold@presicient.com>
Copyright 2003-2006, Philip Crow, all rights reserved. You may modify and/or redistribute this code in the same manner as Perl itself.
| UML-Sequence documentation | Contained in the UML-Sequence distribution. |
package UML::Sequence::Svg; use Exporter; @ISA = qw(Exporter); @EXPORT = qw(seq2svg); use XML::DOM; use Getopt::Std; use strict; use warnings; our $VERSION = '0.02'; # Constant declarations. my $CLASS_TEXT_Y = 40; my $CLASS_BOX_Y = 25; my $CLASS_BOX_HEIGHT = 20; my $CLASS_BOX_WIDTH = 125; my $CLASS_SPACING = 3; my $LEFT_EDGE = 30; my $ACTIVATION_WIDTH = 15; my $ACTIVATION_OFFSET = 10; my $FIRST_ARROW = 55; my $ARROW_SPACING = 40; # Global variable: my $output_file = "-"; my $classcolor = 'white'; my $actcolor = 'white'; my $docpath; # path to assoc. classdocs my $hyperlink = 0; # true => embed hyperlinks my $mappath; # path to write areamap file my $mapname; # imagemap name derived from SVG output filename my $charwidth = 10; # width of characters my $charht = 10; # height of characters my $annot; # annotations behavior: # 'e' : embed in specified file; # 'j' : apply javascript tooltips in specified file # NOTE: specified file may be same as image map file sub seq2svg { local (@ARGV) = @_; my $opts = parse_command_line(); $classcolor = $opts->{c} || 'white'; $actcolor = $opts->{a} || 'white'; $docpath = $opts->{p} || $opts->{P}; $hyperlink = defined($opts->{p}); $mappath = $opts->{m} || $opts->{M}; $charwidth = $opts->{x} || 6; $charht = $opts->{y} || 14; $CLASS_BOX_WIDTH = $opts->{w} || 125; $annot = $opts->{e} ? 'e' : $opts->{j} ? 'j' : undef; die "Annotation requested without output path." if ($annot && (! $mappath)); $ARROW_SPACING = (index($opts->{g}, '.') >= 0) ? int($ARROW_SPACING * $opts->{g}) : $opts->{g} if $opts->{g}; $docpath .= '/' if ($docpath && (substr($docpath, -1, 1) ne '/')); my $input_file = shift @ARGV; if (defined $input_file) { open INPUT, "$input_file" or die "Couldn't open $input_file for input: $!\n"; } else { *INPUT = *STDIN; } # # DAA Add HTML image map rendering # my $mapfd; if (defined($mappath)) { $mappath = $opts->{M} ? ">>$mappath" : ">$mappath"; die "Cannot open image map file $mappath: $!" unless open($mapfd, $mappath); # # define a mapname from output file name (if any) # $mapname = ($output_file=~/(\w+)\.\w+$/) ? $1 : 'mapname'; print $mapfd "<html> <body> <img src='$mapname\.png' usemap='#$mapname'> <MAP NAME='$mapname'> "; } my $parser = XML::DOM::Parser->new(); my $doc = $parser->parse(*INPUT); my $sequence = $doc->getDocumentElement(); my $title = $sequence->getAttribute("title"); my $classes = $doc->getElementsByTagName("class"); # # DAA added to track previous class for drawing async arrows # my %priors = (); my $class_output = draw_classes($classes, $mapfd, \%priors); my $class_hash = build_class_name_hash($classes); my $arrow_output = draw_arrows($doc, $class_hash, $mapfd, \%priors); my $class_count = scalar (keys %$class_hash); my $arrow_count = count_arrows($doc); my $width = ($class_count + 1) * ($CLASS_BOX_WIDTH + $CLASS_SPACING) + 40; my $height = 2.5 * $CLASS_TEXT_Y + $arrow_count * ($ARROW_SPACING); open SVGOUT, ">$output_file"; print SVGOUT <<EOJ; <?xml version="1.0"?> <svg xmlns="http://www.w3.org/2000/svg" height="$height" width="$width"> <defs> <style type="text/css"> rect, line, path { stroke-width: 2; stroke: black } text { font-weight: bold } <marker orient="auto" refY="2.5" refX="4" markerHeight="7" markerWidth="6" id="mArrow"> <path style="fill: black; stroke: none" d="M 0 0 6 3 0 7"/> </marker> <marker orient="auto" refY="2.5" refX="4" markerHeight="7" markerWidth="6" id="mRtHalfArrow"> <path style="fill: black; stroke: none" d="M 0 7 6 2 0 2"/> </marker> <marker orient="0 deg" refY="2.5" refX="4" markerHeight="7" markerWidth="6" id="mLtHalfArrow"> <path style="fill: black; stroke: none" d="M 0 2 6 2 6 7"/> </marker> </style> </defs> EOJ if ($title) { print SVGOUT <<EOJ; <text x="5" y="15"> $title </text> EOJ } print SVGOUT <<EOJ; $class_output$arrow_output</svg> EOJ # # DAA terminate areamap # if (defined($mapfd)) { print $mapfd ' <script language="JavaScript" type="text/javascript" src="../wz_tooltip.js"></script> </body> </html> ' if $opts->{j}; close $mapfd; } } sub draw_classes { my $classes = shift; my $mapfd = shift; my $priors = shift; my $retval; my $x = $LEFT_EDGE; my $box_left = $LEFT_EDGE - 8; my $y = $CLASS_TEXT_Y; my $max_extent; my $boxht; my $boxtext; my $prior = '_EXTERNAL'; for (my $i = 0; $i < $classes->getLength(); $i++) { my $class = $classes->item($i); my $life_x = int($x + $CLASS_BOX_WIDTH / 2); my $class_name = $class ->getAttribute("name"); if ($class_name eq '_EXTERNAL') { $retval = ''; } else { $priors->{$class_name} = $prior; $prior = $class_name; ($boxht, $boxtext) = _wrapText($class_name, $x); # # DAA add hyperlink to psichedocs # my $class_path = ($docpath && ($class_name!~/\s/)) ? $docpath . join('/', split(/::/, $class_name)) . '.html' : undef; my $born = $class ->getAttribute("born") * $ARROW_SPACING + $FIRST_ARROW; my $extends_to = ($class ->getAttribute("extends-to") + 1) * $ARROW_SPACING + $FIRST_ARROW; if (not defined $max_extent) { $max_extent = $extends_to; } # # DAA rearranged to place text on top of rectangle # for fill purposes # $retval .= " <rect style='fill: $classcolor' height='$boxht' " . "width='$CLASS_BOX_WIDTH' y='$CLASS_BOX_Y' x='$box_left' />\n"; $retval .= "<a xlink:href='$class_path'>" if $hyperlink; $retval .= "<text y='$CLASS_TEXT_Y' x='$x'>$boxtext</text>\n"; $retval .= "</a>" if $hyperlink; # # DAA support areamaps # print $mapfd "<AREA TITLE='$class_name'", ($class_path ? " HREF='$class_path'" : ''), " SHAPE=RECT COORDS='$box_left,$CLASS_BOX_Y,", $box_left + $CLASS_BOX_WIDTH, ',', $CLASS_BOX_Y + $boxht, "'>\n" if $mapfd; $retval .= " <line style='stroke-dasharray: 4,4; ' fill='none' " . "stroke='black' x1='$life_x' y1='$born' x2='$life_x' " . "y2='$max_extent' />\n"; my $activation_x = int($box_left + $CLASS_BOX_WIDTH / 2); my @activations = $class->getElementsByTagName("activation"); foreach my $activation (@activations) { my $born = $activation->getAttribute("born"); my $extends_to = $activation->getAttribute("extends-to"); my $offset = $activation->getAttribute("offset"); my $top = $FIRST_ARROW + $born * $ARROW_SPACING; my $height = ($extends_to - $born + .5) * $ARROW_SPACING; my $left = $activation_x + $offset * $ACTIVATION_OFFSET; $retval .= " <rect style='fill: $actcolor' height='$height' " . "width='$ACTIVATION_WIDTH' y='$top' x='$left'/>\n"; } } $x += $CLASS_BOX_WIDTH + $CLASS_SPACING; $box_left += $CLASS_BOX_WIDTH + $CLASS_SPACING; $retval .= "\n"; } return $retval; } sub count_arrows { my $doc = shift; my $arrows = $doc->getElementsByTagName("arrow"); return $arrows->getLength(); } sub draw_arrows { my $doc = shift; my $class_hash = shift; my $mapfd = shift; my $priors = shift; my $retval; my $arrows = $doc->getElementsByTagName("arrow"); my $annotnum = 1; my $annotspan = "\n<ol>\n" if ($annot && ($annot eq 'e')); for (my $i = 0; $i < $arrows->getLength(); $i++) { my $arrow = $arrows->item($i); my $from = $arrow->getAttribute("from" ); my $to = $arrow->getAttribute("to" ); # # DAA 12/24/2005 # use type attribute to specify returnvalue or external # which changes the line style to dashed or folded, respectively # also note that $from for external events originate at the far left # my $type = $arrow->getAttribute("type" ); my $label = $arrow->getAttribute("label" ); my $from_offset = $arrow->getAttribute("from-offset"); my $to_offset = $arrow->getAttribute("to-offset" ); my $annots = $arrow->getElementsByTagName('annotation'); $annots = $annots->item(0) if $annots; my $y = $FIRST_ARROW + ($i + 1) * $ARROW_SPACING; my $from_number = ($from eq '_EXTERNAL') ? $class_hash->{$priors->{$to}} : $class_hash->{$from}; my $to_number = $class_hash->{$to}; $label =~ s/</</g; $label =~ s/>/>/g; my $class_path; my $labmethod = $hyperlink ? \&drawHyperLabel : \&drawLabel; # # DAA add hyperlink to psichedocs # if ($docpath && ($to!~/\s/)) { my $doclabel = $label; $doclabel=~s/[\*!]//g; $doclabel=~s/^\s*\[[^\]]*\]\s*//; $doclabel=~s/^\s+//; $doclabel=~s/\s+$//; $class_path = $docpath . join('/', split(/::/, $to)) . ".html#$doclabel" unless ($doclabel=~/\s/); } if ($from_number < $to_number) { # arrow from left to right my $x1 = $from_number * ($CLASS_BOX_WIDTH + $CLASS_SPACING) + $LEFT_EDGE + ($CLASS_BOX_WIDTH + $ACTIVATION_WIDTH)/2 + $from_offset * $ACTIVATION_OFFSET; $x1 += 20 if ($from eq '_EXTERNAL'); my $x2 = $to_number * ($CLASS_BOX_WIDTH + $CLASS_SPACING) + $LEFT_EDGE + ($CLASS_BOX_WIDTH - $ACTIVATION_WIDTH)/2; # # DAA cuddle the label to the arrowhead # my $xlab = $x2 - $CLASS_SPACING - 6 - ($label ? ($charwidth * length($label)) : $charwidth); my $ylab = $y - 6; my $xend = $label ? $xlab + ($charwidth * length($label)) : undef; my $yend = $ylab + $charht; # # DAA changed to support call vs. return vs. async activations # $retval .= "<line x1='$x1' y1='$y' x2='$x2' y2='$y' style='" . (($type eq 'return') ? "stroke-dasharray: 4,4; " : ' ') . 'marker-end: url(#' . (($type eq 'async') ? "mRtHalfArrow);' />\n" : "mArrow);' />\n"); # # DAA need to add hrefs for methods, but we'll need to track the "to" class # name, and ignore names w/ whitespace # $retval .= $labmethod->($mapfd, $xlab, $ylab, $xend, $yend, $type, $label, $class_path, $mapname, $i, \$annotnum, $annots, \$annotspan) if defined($label); } elsif ($from_number > $to_number) { # arrow from right to left my $x1 = $from_number * ($CLASS_BOX_WIDTH + $CLASS_SPACING) + $LEFT_EDGE + ($CLASS_BOX_WIDTH - $ACTIVATION_WIDTH)/2; my $x2 = $to_number * ($CLASS_BOX_WIDTH + $CLASS_SPACING) + $LEFT_EDGE + ($CLASS_BOX_WIDTH + $ACTIVATION_WIDTH)/2 + $to_offset * $ACTIVATION_OFFSET; # # DAA changed to support call vs. return vs. async activations # (note async activations always go from left to right, so we won't get # any here...) # $retval .= "<line x1='$x1' y1='$y' x2='$x2' y2='$y' style='" . (($type eq 'return') ? "stroke-dasharray: 4,4; " : ' ') . "marker-end: url(#mArrow);' />\n"; my $xlab = $x2 + $CLASS_SPACING + 6; my $ylab = $y - 6; my $xend = $label ? $xlab + ($charwidth * length($label)) : undef; my $yend = $ylab + $charht; # # DAA need to add hrefs for methods, but we'll need to track the "to" class # name, and ignore names w/ whitespace # $retval .= $labmethod->($mapfd, $xlab, $ylab, $xend, $yend, $type, $label, $class_path, $mapname, $i, \$annotnum, $annots, \$annotspan) if defined($label); } else { # arrow from and to same class my $x1 = $from_number * ($CLASS_BOX_WIDTH + $CLASS_SPACING) + $LEFT_EDGE + ($CLASS_BOX_WIDTH + $ACTIVATION_WIDTH)/2 + $from_offset * $ACTIVATION_OFFSET; my $x2 = $to_number * ($CLASS_BOX_WIDTH + $CLASS_SPACING) + $LEFT_EDGE + ($CLASS_BOX_WIDTH + $ACTIVATION_WIDTH)/2 + $to_offset * $ACTIVATION_OFFSET; $y -= 10; my $y2 = $y + 20; my $x1padded = $x1 + $ACTIVATION_OFFSET + 15; $retval .= "<line x1='$x1' y1='$y' x2='$x1padded' y2='$y' />\n" . "<line x1='$x1padded' y1='$y' x2='$x1padded' y2='$y2' />\n" . "<line x1='$x1padded' y1='$y2' x2='$x2' y2='$y2' " . "style='marker-end: url(#mArrow);' />\n"; my $xlab = $x1padded + $CLASS_SPACING; my $ylab = ($y + $y2) / 2; my $xend = $label ? $xlab + ($charwidth * length($label)) : undef; my $yend = $ylab + $charht; # # DAA need to add hrefs for methods, but we'll need to track the "to" class # name, and ignore names w/ whitespace # $retval .= $labmethod->($mapfd, $xlab, $ylab, $xend, $yend, $type, $label, $class_path, $mapname, $i, \$annotnum, $annots, \$annotspan) if defined($label); } } # # DAA save annotations if any # if ($mapfd) { print $mapfd "</MAP>\n\n"; print $mapfd "$annotspan\n</ol>\n" if $annotspan && ($annotspan ne '<ol>'); } return $retval; } sub build_class_name_hash { my $class_nodes = shift; my %classes; # keyed by class name store left to right position for (my $i = 0; $i < $class_nodes->getLength(); $i++) { my $class = $class_nodes->item($i); my $class_name = $class->getAttribute("name"); $classes{$class_name} = $i; } return \%classes; } # # DAA to wrap long class names in the header boxes # sub _wrapText { my ($text, $xpos) = @_; return ($CLASS_BOX_HEIGHT, $text) if ((length($text) * $charwidth) < ($CLASS_BOX_WIDTH - 10)); # # split on whitespace, dot/colon/underscore, or # lowercaseUppercase # my $maxChars = int(($CLASS_BOX_WIDTH - 10)/$charwidth); my @lines = (); my @pieces = (); while ($text=~s/([^:\.\s_]+)((:+)|\.|_|\s+)?//) { my ($t, $p) = ($1, $2); push @pieces, $t; # # if the text is still too long, look for lowerUpper # if (length($pieces[-1]) <= $maxChars) { push(@pieces, $p) if $p; next; } $t = pop @pieces; push @pieces, $1 while ($t=~s/^(.*?[a-z])([A-Z].*)$/$2/); push @pieces, $t if ($t ne ''); push(@pieces, $p) if $p; } # # now reassemble to minimize box height # $lines[0] = shift @pieces; foreach (@pieces) { $lines[-1] .= $_, next if (length($lines[-1]) <= $maxChars) && ((length($lines[-1]) + length($_)) <= $maxChars); push @lines, $_ unless /^\s+$/; } my $otext = "<tspan x='$xpos'>" . join("</tspan>\n<tspan x='$xpos' dy='1em'>", @lines) . '</tspan>'; return (($charht + 2) * scalar @lines, $otext); } sub parse_command_line { my %opts; getopts('a:c:eg:jm:M:o:p:P:w:x:y:', \%opts); $output_file = $opts{o} if defined $opts{o}; $classcolor = $opts{c} || 'white'; $actcolor = $opts{a} || 'white'; $docpath = $opts{p} || $opts{P}; $annot = $opts{e} || $opts{j}; return \%opts; } sub _createLabelMap { my ($x1, $y1, $x2, $y2, $title, $name, $path, $annot_text, $annot_name, $annot_span) = @_; # # NOTE: Batik seems to render the text coords about 10px lower than SVG, # so we'll cheat here... # $y1 -= 10, $y2 -= 10; my $maptext = "<AREA NAME='$name' TITLE='$title' SHAPE=RECT COORDS='$x1,$y1,$x2,$y2' " . ($path ? "HREF='$path' " : ''); if ($annot_text) { if ($annot && ($annot eq 'j')) { # # Scripted annotation # $annot_text =~s/'/\\'/g; # escape single quotes $title =~s/'/\\'/g; # escape single quotes # # NOTE: this must all be on 1 line!!! # $maptext .=" onmouseover=\"this.T_STATIC=true;this.T_FONTCOLOR='black';this.T_FONTSIZE='12px';this.T_BGCOLOR='#e0e0e0';this.T_OPACITY=90;this.T_SHADOWWIDTH=8;return escape('$annot_text')\" > "; } else { # annot eq 'e' # # embedded annotation, add anchor and annotation text # NOTE: the offsets used here are heuristic, need a bbox jscript # my ($x3, $y3, $y4) = ($x2+4, $y1 + (($y2 - $y1)>>1), $y1 - 4); $maptext .= "> <AREA SHAPE=RECT COORDS='$x2,$y4,$x3,$y3' HREF='#$annot_name'> "; $annot_text=~s/</</g; $annot_text=~s/>/>/g; $$annot_span .= " <a name='$annot_name'></a> <li>$annot_text <p> "; } } else { $maptext .= ">\n"; } return $maptext; } sub drawLabel { my ($mapfd, $xlab, $ylab, $xend, $yend, $type, $label, $path, $mapname, $i, $annotnum, $annots, $annotspan) = @_; my $retval = "<text x='$xlab' y='$ylab' " . # # use italics for asyncs # (($type eq 'async') ? "style='font-style: italic;'><tspan>" : '><tspan>') . $label; # # if annotated, add superscript if embedded # $retval .= "<tspan baseline-shift=\"super\">$$annotnum</tspan>", $$annotnum++ if ($annot && ($annot eq 'e') && $annots); $retval .= "</tspan></text>\n"; # # DAA support areamaps # print $mapfd _createLabelMap( $xlab, $ylab, $xend, $yend, # location $label, "$mapname\_$i", (($type eq 'call') ? $path : undef), ($annots ? $annots->getAttribute('text') : undef), "$mapname\_annot_$i", $annotspan) # annotation if $mapfd; return $retval; } sub drawHyperLabel { my ($mapfd, $xlab, $ylab, $xend, $yend, $type, $label, $path, $mapname, $i, $annotnum, $annots, $annotspan) = @_; my $retval = "<text x='$xlab' y='$ylab' " . # # use italics for asyncs # (($type eq 'async') ? "style='font-style: italic;'><tspan>" : '><tspan>') . ($path ? "<a xlink:href='$path'>$label</a>" : $label); # # if annotated, add superscript if embedded # $retval .= "<tspan baseline-shift=\"super\"> <a xlink:href='#$mapname\_annot_$i'>$$annotnum</a> </tspan>", $$annotnum++ if ($annot && ($annot eq 'e') && $annots); $retval .= "</tspan></text>\n"; # # DAA support areamaps # print $mapfd _createLabelMap( $xlab, $ylab, $xend, $yend, # location $label, "$mapname\_$i", (($type eq 'call') ? $path : undef), ($annots ? $annots->getAttribute('text') : undef), "$mapname\_annot_$i", $annotspan) # annotation if $mapfd; return $retval; } 1;