File Coverage

lib/Gtk2/Ex/MindMapView/Layout/Balanced.pm
Criterion Covered Total %
statement 13 15 86.6
branch n/a
condition n/a
subroutine 5 5 100.0
pod n/a
total 18 20 90.0


line stmt bran cond sub pod time code
1             package Gtk2::Ex::MindMapView::Layout::Balanced;
2              
3             our $VERSION = '0.000001';
4              
5 1     1   1742 use warnings;
  1         2  
  1         33  
6 1     1   5 use strict;
  1         2  
  1         29  
7 1     1   5 use Carp;
  1         2  
  1         62  
8              
9 1     1   6 use List::Util;
  1         1  
  1         49  
10              
11 1     1   10642 use Glib ':constants';
  0            
  0            
12              
13             use constant SLACK_MANY_ITEMS=>1.0;
14             use constant SLACK_FEW_ITEMS=>1.25;
15              
16             use Gtk2::Ex::MindMapView::Layout::Group;
17             use Gtk2::Ex::MindMapView::Layout::Column;
18              
19             use base 'Gtk2::Ex::MindMapView::Layout::Group';
20              
21             sub new
22             {
23             my $class = shift(@_);
24              
25             my $self = $class->SUPER::new(@_);
26              
27             if (!defined $self->{graph})
28             {
29             croak "You must pass a Gtk2::Ex::MindMapView::Graph as argument.\n";
30             }
31              
32             $self->{lhs_weight} = 0; # Weight of left hand side of tree.
33              
34             $self->{rhs_weight} = 0; # Weight of right hand side of tree.
35              
36             $self->{columns} = {}; # Hash of columns.
37              
38             $self->{item_count} = 0; # A count of the number of items in the graph.
39              
40             $self->{allocated} = {}; # List of items that have been allocated.
41              
42             my $root = $self->{graph}->get_root();
43              
44             $self->{graph}->traverse_BFS($root, sub { _allocate($self, $_[0]); });
45              
46             return $self;
47             }
48              
49              
50             # $layout->layout()
51              
52             sub layout
53             {
54             my $self = shift(@_);
55              
56             my $lhs_offset = 0; # Offset of columns on left hand side of tree.
57              
58             my $rhs_offset = 0; # Offset of columns on right hand side of tree.
59              
60             my @columns = values (%{$self->{columns}});
61              
62             my @sorted_columns = sort { abs($a->get('column_no')) <=> abs($b->get('column_no')) } @columns;
63              
64             foreach my $column (@sorted_columns)
65             {
66             my $column_no = $column->get('column_no');
67              
68             my $width = $column->get('width') + $self->get_horizontal_padding();
69              
70             $column->set(y=>(-0.5 * $column->get('height')));
71              
72             if ($column_no > 0)
73             {
74             $column->set(x=>$rhs_offset);
75              
76             $rhs_offset += $width;
77             }
78              
79             if ($column_no < 0)
80             {
81             $lhs_offset -= $width;
82              
83             $column->set(x=>$lhs_offset);
84             }
85              
86             if ($column_no == 0)
87             {
88             $rhs_offset = $width / 2;
89              
90             $lhs_offset = -($width - $rhs_offset);
91              
92             $column->set(x=>$lhs_offset);
93             }
94              
95             $column->layout();
96             }
97             }
98              
99              
100             sub _allocate
101             {
102             my ($self, $item) = @_;
103              
104             my @predecessors = $self->{graph}->predecessors($item);
105              
106             my $num_predecessors = scalar (@predecessors);
107              
108             # print "Balanced, _allocate, item: $item num_predecessors: $num_predecessors\n";
109              
110             if ($num_predecessors == 0) # we're the root item.
111             {
112             _add($self, undef, $item, 0);
113              
114             return;
115             }
116              
117             if ($num_predecessors == 1) # single predecessor.
118             {
119             my $column_no = _next_column_no($self, $predecessors[0], $item);
120              
121             _add($self, $predecessors[0], $item, $column_no);
122              
123             return;
124             }
125              
126             # Multiple predecessors.
127              
128             my @visible_predecessors = grep { $_->is_visible(); } @predecessors;
129              
130             if (scalar @visible_predecessors == 0) # FIXME: dubious.
131             {
132             _add($self, $predecessors[0], $item, 0);
133              
134             return;
135             }
136              
137             if (scalar @visible_predecessors == 1)
138             {
139             my $column_no = _next_column_no($self, $visible_predecessors[0], $item);
140              
141             _add($self, $visible_predecessors[0], $item, $column_no);
142              
143             return;
144             }
145              
146             # Multiple visible predecessors.
147              
148             my @column_nos = map { $_->get_column_no(); } @visible_predecessors;
149              
150             if (_all_same(@column_nos))
151             {
152             my $column_no = _next_column_no($self, $visible_predecessors[0], $item);
153              
154             _add($self, $visible_predecessors[0], $item, $column_no);
155              
156             return;
157             }
158              
159             # "Average" column number.
160              
161             my $total = List::Util::sum(@column_nos);
162              
163             my $column_no = int($total / (scalar @visible_predecessors));
164              
165             _add($self, $visible_predecessors[0], $item, $column_no);
166             }
167              
168              
169             sub _all_same
170             {
171             my $first = shift(@_);
172              
173             foreach my $next (@_)
174             {
175             return FALSE if ($next != $first);
176             }
177              
178             return TRUE;
179             }
180              
181              
182             sub _add
183             {
184             my ($self, $predecessor_item, $item, $column_no) = @_;
185              
186             return if (exists $self->{allocated}{$item});
187              
188             my $column = $self->{columns}{$column_no};
189              
190             if (!defined $column)
191             {
192             $column = Gtk2::Ex::MindMapView::Layout::Column->new(column_no=>$column_no);
193              
194             $self->{columns}{$column_no} = $column;
195             }
196              
197             $item->set(column=>$column);
198              
199             $column->add($predecessor_item, $item);
200              
201             $self->{allocated}{$item} = 1;
202              
203             my @columns = values (%{$self->{columns}});
204              
205             $self->set(height=>_balanced_height($self, \@columns));
206              
207             $self->set(width=>_balanced_width($self, \@columns));
208              
209             $self->{item_count}++;
210             }
211              
212              
213             sub _balanced_height
214             {
215             my ($self, $columns_ref) = @_;
216              
217             my @columns = @$columns_ref;
218              
219             return 0 if (scalar @columns == 0);
220              
221             return List::Util::max( map { $_->get('height'); } @columns );
222             }
223              
224              
225             sub _balanced_width
226             {
227             my ($self, $columns_ref) = @_;
228              
229             my @columns = @$columns_ref;
230              
231             return 0 if (scalar @columns == 0);
232              
233             return List::Util::sum( map { $_->get('width'); } @columns );
234             }
235              
236              
237             sub _next_column_no
238             {
239             my ($self, $predecessor_item, $item) = @_;
240              
241             my $column_no = $predecessor_item->get_column_no();
242              
243             return ($column_no + 1) if ($column_no > 0);
244              
245             return ($column_no - 1) if ($column_no < 0);
246              
247             if ($self->{rhs_weight} > ($self->{lhs_weight} * _slack($self)))
248             {
249             $self->{graph}->traverse_DFS($item, sub { $self->{lhs_weight} += $_[0]->get_weight(); });
250              
251             return -1;
252             }
253              
254             $self->{graph}->traverse_DFS($item, sub { $self->{rhs_weight} += $_[0]->get_weight(); });
255              
256             return 1;
257             }
258              
259              
260             # _slack: Make rebalancing less "touchy" when the number of items is
261             # small.
262              
263             sub _slack
264             {
265             my $self = shift(@_);
266              
267             if ($self->{item_count} < 10)
268             {
269             return SLACK_FEW_ITEMS; # > 1
270             }
271              
272             return SLACK_MANY_ITEMS; # 1
273             }
274              
275              
276             1; # Magic true value required at end of module
277             __END__