=head1 NAME UML::Sequence::Svg - converts xml sequence files to svg =head1 SYNOPSIS use UML::Sequence::Svg; seq2svg @ARGV; =head1 DESCRIPTION This module supports the seq2svg.pl script like Pod::Html supports pod2html. The array passed to seq2svg.pl should have the following form: ([ I, ] [input_file_name]) where I are any of =over 4 =item C<-a color> specifies a color to be used to fill the activation boxes. Must be in a form acceptable to SVG. =item C<-c color> specifies a color to be used to fill the class boxes. Must be in a form acceptable to SVG. =item C<-e> specifies that embedded annotations are to be applied to any rasterized version of the SVG image. Only valid when the C<-m> or C<-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. =item C<-g arrow-gap> If C is an integer value, specifies number of pixels between arrows (default is 40 pixels). If C is a fractional value, specifies a scaling factor for the default number of pixels between arrows. =item C<-j> specifies that Javascript'ed tooltip annotations are to be applied to any rasterized version of the SVG image. Only valid when the C<-m> or C<-M> option is also specified. When an arrow label has associated annotations, the hyperlinks in the areamap for the label will include C function calls containing the annotation text for use with the Javascript tooltip package available at L. =item C<-m areamap-path>, C<-M areamap-path> specifies the name of a file to receive HTML containing an image element, areamap, and (optionally) either an ordered list of annotations (if L<-e> was specified) or a script tag linking to the Javascript tooltip script (if L<-j> was specified). to be applied to any rasterized version of the SVG image. Only valid when either the L<-p> or L<-P> option is specified. C<-M> specifies append mode for the output file. =item C<-o output_file_name> specifies the output file name. =item C<-p classdocs-path>, C<-P classdocs-path> specifies a base path to classdocs generated by psichedoc. C<-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. C<-P> is the same, except it does B embed hyperlinks in the SVG (due to Batik's current inability to handle rasterization of SVG's with embeded hyperlinks). In other words, use C<-P> to specify a path for HTML imagemaps when the SVG output will be further processed by Batik. =item C<-w box-width> specifies width of class box in pixels; default is 125. Used to compute class header boxes and areamap coordinates. =item C<-x char-width> specifies width of characters in pixels; default is 6. Used to compute class header boxes and areamap coordinates. =item C<-y char-height> specifies height of characters in pixels; default is 14. Used to compute class header boxes and areamap coordinates. =back By default input is from standard in and output is to standard out. =cut 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 " "; } 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 if ($title) { print SVGOUT < $title EOJ } print SVGOUT < EOJ # # DAA terminate areamap # if (defined($mapfd)) { print $mapfd ' ' 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 .= " \n"; $retval .= "" if $hyperlink; $retval .= "$boxtext\n"; $retval .= "" if $hyperlink; # # DAA support areamaps # print $mapfd "\n" if $mapfd; $retval .= " \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 .= " \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
    \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; 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 .= "\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 .= "\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 .= "\n" . "\n" . "\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 "\n\n"; print $mapfd "$annotspan\n
\n" if $annotspan && ($annotspan ne '
    '); } 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 = "" . join("\n", @lines) . ''; 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 = " "; } 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 .= "> "; $annot_text=~s//>/g; $$annot_span .= "
  1. $annot_text

    "; } } else { $maptext .= ">\n"; } return $maptext; } sub drawLabel { my ($mapfd, $xlab, $ylab, $xend, $yend, $type, $label, $path, $mapname, $i, $annotnum, $annots, $annotspan) = @_; my $retval = "" : '>') . $label; # # if annotated, add superscript if embedded # $retval .= "$$annotnum", $$annotnum++ if ($annot && ($annot eq 'e') && $annots); $retval .= "\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 = "" : '>') . ($path ? "$label" : $label); # # if annotated, add superscript if embedded # $retval .= " $$annotnum ", $$annotnum++ if ($annot && ($annot eq 'e') && $annots); $retval .= "\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; =head1 SEE ALSO L =head1 AUTHOR Phil Crow, Version 0.02 Updates by Dean Arnold, =head1 COPYRIGHT Copyright 2003-2006, Philip Crow, all rights reserved. You may modify and/or redistribute this code in the same manner as Perl itself. =cut