File Coverage

blib/lib/UML/State.pm
Criterion Covered Total %
statement 164 187 87.7
branch 22 38 57.8
condition 6 9 66.6
subroutine 16 17 94.1
pod 1 2 50.0
total 209 253 82.6


line stmt bran cond sub pod time code
1             package UML::State;
2 1     1   21924 use strict; use warnings;
  1     1   2  
  1         38  
  1         5  
  1         2  
  1         2359  
3              
4             our $VERSION = "0.02";
5              
6             =head1 NAME
7              
8             UML::State - an object oriented module which draws simple state diagrams
9              
10             =head1 VERSION
11              
12             This documentation covers version 0.01, the initial release made in May 2003.
13              
14             =head1 SYNOPSIS
15              
16             use UML::State;
17              
18             my $diagram = UML::State->new(
19             $node_array,
20             $start_list,
21             $accept_list,
22             $edges
23             );
24              
25             # You may change these defaults (doing so may even work):
26             $UML::State::ROW_SPACING = 75; # all numbers are in pixels
27             $UML::State::LEFT_MARGIN = 20;
28             $UML::State::WIDTH = 800;
29             $UML::State::HEIGHT = 800;
30              
31             print $diagram->draw();
32              
33             =head1 ABSTRACT
34              
35             Are you tired of pointing and clicking to make simple diagrams? Do your
36             wrists hurt thinking about making the pretty UML your boss likes so well?
37             Consider using UML::State and UML::Sequence to make your life easier.
38              
39             UML::State together with drawstate.pl allows you to easily generate
40             state diagrams. You enter them in something like a cross between ASCII
41             art and school room algebra. They come out looking like something from
42             a drawing program like Visio. See drawstate.pl in the distribution for
43             details about the input format and the samples directory for some
44             examples of input and output.
45              
46             =head1 DESCRIPTION
47              
48             You will probably use this class by running drawstate.pl or drawstatexml.pl
49             which are included in the distribution. But you can use this package
50             directly to gain control over the appearance of your pictures.
51              
52             The two methods you need are new and draw (see below). If you want, you
53             may change the dimensions by setting the package global variables as shown
54             in the SYNOPSIS. Obviously, no error checking is done, so be careful to
55             use reasonable values (positive numbers are good). All numbers are in pixels
56             (sorry by Bezier's in SVG seem to require pixels). I have not tried changing
57             the numbers, so I don't have any idea if doing so makes reasonable changes
58             to the output.
59              
60             =head1 EXPORT
61              
62             Nothing, this module is object oriented.
63              
64             =head1 METHODS
65              
66             =cut
67              
68             our $ROW_SPACING = 75;
69             our $LEFT_MARGIN = 20;
70             our $WIDTH = 800;
71             our $HEIGHT = 800;
72              
73             =head2 new
74              
75             This constructor expects the following things:
76              
77             =over 4
78              
79             =item $node_array
80              
81             A reference to a two dimensional array holding the names (and implicit
82             positions) of the nodes in your state graph. If you want to leave a blank
83             space in the diagram, include the empty string as the name of a node you
84             want to omit.
85              
86             Example:
87              
88             $nodes = [ [ "A", "B", "C"],
89             [ "D", "", "E"] ];
90              
91             This is six nodes labeled A-E arranged in two rows with three nodes in
92             each row. The middle node of the second row is omitted.
93              
94             =item $start_list
95              
96             A reference to an array listing the start state edge(s). Each arrow should
97             be of the form: col,row,head_direction tail_direction. The entry
98             $node_array->[col][row] must be defined. The directions can be any of
99             N, S, E, or W representing compass points on the centers of the sides of
100             the node's box. N is the top center, S is the bottom center, etc.
101             The head_direction is the one the arrow points to. 0,0,N N is a common
102             start edge.
103              
104             =item $accept_list
105              
106             A reference to an array listing the accepting states in your graph. Each
107             entry in the array should be an ordered pair col,row. The entry
108             $node_array->[col][row] must be defined. The only affect of a node being
109             in the accept list is to make a doubled box around its name.
110              
111             =item $edges
112              
113             A reference to a hash. Each key in the hash is an edge label. The
114             corresponding value is a list of edges. Each edge is a string with
115             two or three parts. The first two parts are positions and are required.
116             The third element is optional. It controls curving edges. If absent
117             the edge is straight. If present it may match /Counter.*/ for
118             counterclockwise, or anything else for clockwise. Trying some examples will
119             make more sense than what I might write.
120              
121             The positions are of the same form as starting edges: col,row,direction.
122             The tail is listed first, then the head. If you must include a self
123             referencing edge, use exactly the same entry for tail and head.
124             Currently this makes a small circle at that point. Self reference circles
125             have no arrow head.
126              
127             Examples:
128              
129             0,0,S 0,1,N Counter
130             0,1,W 1,1,E
131             1,1,N 1,1,N
132              
133             The first edge connects the south side of the node at 0,0 to the north side
134             of the node at 0,1 with an arc curved in the counter-clockwise direction.
135             The second edge connects the west side of the node at 0,1 to the east side
136             of the node at 1,1. The third edge is a self reference drawn on the north
137             side of node 1,1.
138              
139             =back
140              
141             =cut
142              
143             sub new {
144 1     1 1 27 my $class = shift;
145              
146 1         8 my $self = {
147             nodes => shift,
148             starters => shift,
149             accepting => shift,
150             edges => shift,
151             cols => undef,
152             rows => undef,
153             widths => undef,
154             col_pos => undef,
155             boxes => undef,
156             };
157 1         2 bless $self, $class;
158              
159 1         4 $self->_count_rows_etc();
160 1         5 $self->_find_col_positions();
161              
162 1         2 return $self;
163             }
164              
165             =head1 draw
166              
167             This method can be called any time after the constructor. It returns
168             a string containing the svg for your state graph. You can print that,
169             or parse it with standard XML techniques.
170              
171             =cut
172             sub draw {
173 1     1 0 383 my $self = shift;
174 1         2 my $answer;
175              
176 1         4 $answer = _print_header($WIDTH, $HEIGHT)
177             . $self->_print_nodes()
178             . $self->_print_start_arrows()
179             . $self->_print_accepting();
180              
181 1         2 foreach my $edge_label (keys %{$self->{edges}}) {
  1         4  
182 3         8 $answer .= _print_arrows(
183             $edge_label,
184             $self->{boxes},
185             $self->{edges}{$edge_label}
186             );
187             }
188              
189 1         4 $answer .= _print_footer();
190 1         26 return $answer;
191             }
192              
193             sub _count_rows_etc {
194 1     1   2 my $self = shift;
195 1         1 my $rows = 0;
196 1         2 my $cols = 0;
197 1         2 my $widths = [];
198              
199 1         2 foreach my $row (@{$self->{nodes}}) {
  1         5  
200 4         5 $rows++;
201 4 100       7 if ($cols < @$row) {
202 1         1 $cols = @$row;
203             }
204 4         8 _update_widest_of($widths, $row);
205             }
206 1         2 $self->{rows} = $rows;
207 1         1 $self->{cols} = $cols;
208 1         2 $self->{widths} = $widths;
209             }
210              
211             # Note Well: This is not a class or instance method, DON'T use -> to call it.
212             sub _print_header {
213 1     1   2 my $width = shift;
214 1         2 my $height = shift;
215              
216 1         5 return <
217            
218            
219            
220            
223            
224             markerHeight="5" markerWidth="4" id="mArrow">
225            
226            
227            
228             EOJ
229             }
230              
231             sub _print_nodes {
232 1     1   3 my $self = shift;
233 1         1 my $boxes = [];
234 1         2 my $answer = "";;
235              
236 1         1 my $row_count = 0;
237 1         2 my $box_height = $ROW_SPACING / 2;
238 1         2 foreach my $row (@{$self->{nodes}}) {
  1         3  
239 4         4 my $col_count = 0;
240 4         5 my $text_y = (1 + $row_count) * $ROW_SPACING;
241 4         13 my $box_y = $text_y - $ROW_SPACING * .25 - .05;
242 4         5 my $next_x;
243 4         5 foreach my $node (@$row) {
244 12         30 my $x = $self->{col_pos}[$col_count];
245 12   66     32 $next_x = $self->{col_pos}[$col_count + 1] || $LEFT_MARGIN + $WIDTH;
246 12         13 my $width = .65 * ($next_x - $x); # .25; #$x - $old_x;
247 12 100       22 unless ($node eq '') {
248 7         7 my $text_x = $x + 5;
249 7         31 $answer .= "$node\n";
250 7         8 $x -= .1;
251 7         63 $answer .= "
252             . "width='$width' y='$box_y' x='$x' />\n";
253             }
254 12         38 $boxes->[$col_count][$row_count] = {
255             top => $box_y,
256             left => $x,
257             height => $box_height,
258             width => $width,
259             };
260 12         19 $col_count++;
261             }
262 4         6 $row_count++;
263             }
264 1         2 $self->{boxes} = $boxes;
265 1         5 return $answer;
266             }
267              
268             sub _print_start_arrows {
269 1     1   1 my $self = shift;
270 1         2 my $answer = "";
271              
272 1         2 foreach my $starter (@{$self->{starters}}) {
  1         2  
273 1         4 my ($head_end, $direction) = split /\s+/, $starter;
274 1         4 my ($head_x, $head_y) = _find_end($head_end, $self->{boxes});
275 1 50       9 if (not defined $head_x) {
276 0         0 print STDERR "Bad starting node: $starter: no such node\n";
277 0         0 next;
278             }
279 1         2 my $length = 20;
280 1         511 my ($tail_x, $tail_y);
281 1 50       4 if ($direction eq 'N') {
    0          
    0          
282 1         1 $tail_x = $head_x;
283 1         2 $tail_y = $head_y - $length;
284             }
285             elsif ($direction eq 'W') {
286 0         0 $tail_x = $head_x - $length;
287 0         0 $tail_y = $head_y;
288             }
289             elsif ($direction eq 'S') {
290 0         0 $tail_x = $head_x;
291 0         0 $tail_y = $head_y + $length;
292             }
293             else { # must be East
294 0         0 $tail_x = $head_x + $length;
295 0         0 $tail_y = $head_y;
296             }
297 1         12 $answer .= "
298             . "style='marker-end: url(#mArrow);'/>\n";
299             }
300 1         6 return $answer;
301             }
302              
303             sub _print_accepting {
304 1     1   1 my $self = shift;
305 1         1 my $answer;
306              
307 1         2 foreach my $accepting_state (@{$self->{accepting}}) {
  1         2  
308 2         4 my ($col, $row) = split /,/, $accepting_state;
309 2 50       7 if (not defined $self->{boxes}[$col][$row]{left}) {
310 0         0 print STDERR "Bad accepting state: ($col, $row): no such node\n";
311 0         0 next;
312             }
313 2         4 my $x = $self->{boxes}[$col][$row]{left} + 2;
314 2         3 my $y = $self->{boxes}[$col][$row]{top} + 2;
315 2         4 my $width = $self->{boxes}[$col][$row]{width} - 4;
316 2         4 my $height = $self->{boxes}[$col][$row]{height} - 4;
317 2         17 $answer .= "
318             . "width='$width' y='$y' x='$x' />\n";
319             }
320 1         2 return $answer;
321             }
322              
323             sub _print_arrows {
324 3     3   3 my $label = shift;
325 3         4 my $boxes = shift;
326 3         2 my $arrows = shift;
327 3         3 my $answer = "";
328              
329 3         5 foreach my $arrow (@$arrows) {
330             # bez is short for Bezier.
331 11         20 my ($tail_desc, $head_desc, $bez) = split /\s+/, $arrow;
332 11         21 my ($tail_x, $tail_y) = _find_end($tail_desc, $boxes);
333 11         16 my ($head_x, $head_y) = _find_end($head_desc, $boxes);
334 11 50 33     39 unless (defined $head_x and defined $tail_x) {
335 0         0 print STDERR "Bad arrow: $arrow: missing node\n";
336 0         0 next;
337             }
338 11         18 my ($text_x, $text_y) = _find_label_pos(
339             $tail_x, $tail_y, $head_x, $head_y
340             );
341 11 100       22 if ($bez) {
    50          
342 2         2 my ($cx, $cy, $t_control, $t_text);
343 2 100       9 if ($bez =~ /Counter/i) { # counter clockwise
344 1         1 $t_control = .25;
345             }
346             else { # clockwise
347 1         1 $t_control = -.25;
348             }
349 2         3 $t_text = $t_control / 2;
350              
351             # To calculate the quadratic Bezier control point, I use the
352             # parametric equations of the line perpendicular to the line
353             # joining the end points. In those equations I make t = .25
354             # or -.25 depending on the user's desired rotation (see the
355             # if directly above).
356 2         3 $cx = ($tail_y - $head_y) * $t_control + .5 * ($head_x + $tail_x);
357 2         3 $cy = ($head_x - $tail_x) * $t_control + .5 * ($head_y + $tail_y);
358              
359             # Drawing as you read the following will be helpful.
360             # Positioning the text requires three steps. First, I find
361             # the point at the intersection of the Bezier curve and the
362             # perpendicular bisector of the line segment joining the end
363             # points. (That line also passes through the control point.)
364             # The point I want is the midpoint along the perpendicular
365             # bisector between the control point and the midpoint of
366             # the segment connecting the end points. Second, since SVG
367             # text boxes are controlled by the LOWER LEFT corner, I must
368             # translate the label to center it on the point I found in
369             # step 1. Third, I need to translate the label off of the curve
370             # by a fixed distance along the line used in part 1 in the
371             # direction of the control point. In practice step 2 is easy
372             # and I do it in combination with the translation for step 3.
373              
374             # Step 1. Find the tangent point on the Bezier curve.
375 2         4 $text_x = ($tail_y - $head_y) * $t_text + .5 * ($head_x + $tail_x);
376 2         3 $text_y = ($head_x - $tail_x) * $t_text + .5 * ($head_y + $tail_y);
377             # ($text_x, $text_y) is now on the tangent to the curve on the
378             # line between the control point and the midpoint between the
379             # end points of the curve. Since text is fixed at the bottom
380             # left point in SVG, we must translate the point to keep it
381             # off the curve, but close to it.
382              
383             # Find the midpoint of the segment connecting the end points.
384 2         2 my ($mid_x, $mid_y);
385 2         3 $mid_x = ($head_x + $tail_x) / 2;
386 2         2 $mid_y = ($head_y + $tail_y) / 2;
387              
388             # Make a unit vector from the mid point I just found,
389             # to the control point.
390 2         2 my ($text_vector_x, $text_vector_y);
391 2         60 my $len = sqrt(($mid_x - $cx)**2 + ($mid_y - $cy)**2);
392 2         3 $text_vector_x = ($cx - $mid_x) / $len;
393 2         2 $text_vector_y = ($cy - $mid_y) / $len;
394              
395             # $text_vector now has a unit vector from the midpoint between the
396             # connected points and the control point.
397              
398             # Steps 2 and 3. Apply the translations.
399             # Note that y increases down the screen, x increases in the
400             # usual direction to the right.
401 2         3 $text_x -= 4 - 10 * $text_vector_x;
402 2         3 $text_y += 4 + 10 * $text_vector_y;
403 2         21 $answer .= "
404             . "style='marker-end: url(#mArrow);' />\n";
405             }
406             elsif ($tail_desc eq $head_desc) {
407 0         0 my ($center_x, $center_y) = _find_self_center($tail_desc, $boxes);
408 0         0 $answer .= "
409             . "style='stroke: black; fill: none;' />\n";
410             }
411             else {
412 9         68 $answer .= "
413             . "x2='$head_x' y2='$head_y' "
414             . "style='marker-end: url(#mArrow);'/>\n";
415             }
416 11         53 $answer .= "$label\n";
417             }
418 3         12 return $answer;
419             }
420              
421             # Note Well: This is not a class or instance method, DON'T use -> to call it.
422             sub _find_self_center {
423 0     0   0 my $desc = shift;
424 0         0 my $boxes = shift;
425 0         0 my (undef, undef, $direction) = split /,/, $desc;
426 0         0 my ($x, $y) = _find_end($desc, $boxes);
427              
428 0 0       0 if ($direction eq 'N') { return ($x, $y - 15); }
  0 0       0  
    0          
429 0         0 elsif ($direction eq 'S') { return ($x, $y + 15); }
430 0         0 elsif ($direction eq 'W') { return ($x - 15, $y); }
431 0         0 else { return ($x + 15, $y); }
432             # else is for E which is the default
433             }
434              
435             # Note Well: This is not a class or instance method, DON'T use -> to call it.
436             sub _print_footer {
437 1     1   2 return "\n";
438             }
439              
440             sub _find_end {
441 23     23   24 my $desc = shift;
442 23         21 my $boxes = shift;
443 23         39 my ($col, $row, $side) = split /,/, $desc;
444 23         22 my ($x, $y);
445              
446 23 50       44 return (undef, undef) unless (defined $boxes->[$col][$row]);
447 23 100       50 if ($side eq 'N') {
    100          
    100          
448 4         7 $x = $boxes->[$col][$row]{left} + .5 * $boxes->[$col][$row]{width};
449 4         5 $y = $boxes->[$col][$row]{top};
450             }
451             elsif ($side eq 'W') {
452 6         7 $x = $boxes->[$col][$row]{left};
453 6         9 $y = $boxes->[$col][$row]{top} + .5 * $boxes->[$col][$row]{height};
454             }
455             elsif ($side eq 'S') {
456 6         11 $x = $boxes->[$col][$row]{left} + .5 * $boxes->[$col][$row]{width};
457 6         9 $y = $boxes->[$col][$row]{top} + $boxes->[$col][$row]{height};
458             }
459             else { # assume they want E
460 7         10 $x = $boxes->[$col][$row]{left} + $boxes->[$col][$row]{width};
461 7         13 $y = $boxes->[$col][$row]{top} + .5 * $boxes->[$col][$row]{height};
462             }
463 23         44 return ($x, $y);
464             }
465              
466             sub _find_label_pos {
467 11     11   9 my $x1 = shift;
468 11         9 my $y1 = shift;
469 11         10 my $x2 = shift;
470 11         9 my $y2 = shift;
471 11         13 my $midx = ($x1 + $x2) * .5 - 10;
472 11         11 my $midy = ($y1 + $y2) * .5 - 3;
473              
474 11         17 return ($midx, $midy);
475             }
476              
477             sub _update_widest_of {
478 4     4   4 my $widths = shift;
479 4         5 my $elements = shift;
480              
481 4         8 for (my $i = 0; $i < @$elements; $i++) {
482 12         16 my $width_guess = 20 + .5 * length $elements->[$i];
483 12 100 100     45 if (not defined $widths->[$i] or $widths->[$i] < $width_guess) {
484 6         13 $widths->[$i] = $width_guess;
485             }
486             }
487             }
488              
489             sub _sum_widths {
490 1     1   1 my $widths = shift;
491 1         2 my $total = 0;
492              
493 1         1 foreach my $width (@$widths) {
494 3         17 $total += $width;
495             }
496 1         17 return $total;
497             }
498              
499             sub _find_col_positions {
500 1     1   2 my $self = shift;
501 1         2 my $col_positions = [];
502              
503 1         4 my $char_width = _sum_widths($self->{widths});
504              
505 1         2 my $x = $LEFT_MARGIN;
506 1         13 foreach my $col (1..$self->{cols}) {
507 3         5 $col_positions->[$col - 1] = $x;
508 3         7 my $allocation = $self->{widths}[$col - 1]/$char_width;
509 3         4 $x += $WIDTH * $allocation;
510 3         8 $x = int($x * 100) / 100.0;
511             }
512 1         4 $self->{col_pos} = $col_positions;
513             }
514              
515             1;
516              
517             =head1 BUGS
518              
519             Self reference edges are just circles, they don't have arrows.
520              
521             There is no way to control the placement of labels.
522              
523             Only one letter labels look good.
524              
525             Resizing (changing the class constants) is unreliable.
526              
527             =head1 AUTHOR
528              
529             Phil Crow Ephilcrow2000@yahoo.com
530              
531             =head1 COPYRIGHT AND LICENSE
532              
533             Copyright 2003 by Phil Crow. All rights reserved. This is free software.
534             You may modify and/or redistribute it under the same terms as Perl 5.8.0.
535              
536             =cut