View Javadoc

1   /*
2   
3       dsh-piccolo-venn  Piccolo2D venn diagram nodes and supporting classes.
4       Copyright (c) 2009-2013 held jointly by the individual authors.
5   
6       This library is free software; you can redistribute it and/or modify it
7       under the terms of the GNU Lesser General Public License as published
8       by the Free Software Foundation; either version 3 of the License, or (at
9       your option) any later version.
10  
11      This library is distributed in the hope that it will be useful, but WITHOUT
12      ANY WARRANTY; with out even the implied warranty of MERCHANTABILITY or
13      FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public
14      License for more details.
15  
16      You should have received a copy of the GNU Lesser General Public License
17      along with this library;  if not, write to the Free Software Foundation,
18      Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307  USA.
19  
20      > http://www.fsf.org/licensing/licenses/lgpl.html
21      > http://www.opensource.org/licenses/lgpl-license.php
22  
23  */
24  package org.dishevelled.piccolo.venn;
25  
26  import java.awt.BasicStroke;
27  import java.awt.Color;
28  import java.awt.Paint;
29  import java.awt.Shape;
30  import java.awt.Stroke;
31  import java.awt.geom.Area;
32  import java.awt.geom.Point2D;
33  import java.awt.geom.Rectangle2D;
34  import java.util.ArrayList;
35  import java.util.HashMap;
36  import java.util.List;
37  import java.util.Map;
38  import java.util.Set;
39  import java.util.concurrent.Executors;
40  import java.util.concurrent.ExecutorService;
41  
42  import javax.swing.SwingUtilities;
43  import javax.swing.SwingWorker;
44  
45  import com.google.common.collect.ImmutableSet;
46  import com.google.common.collect.Sets;
47  
48  import org.dishevelled.bitset.MutableBitSet;
49  import org.dishevelled.bitset.ImmutableBitSet;
50  import org.dishevelled.venn.VennLayout;
51  import org.dishevelled.venn.VennModel;
52  import org.piccolo2d.PNode;
53  import org.piccolo2d.nodes.PArea;
54  import org.piccolo2d.nodes.PPath;
55  import org.piccolo2d.nodes.PText;
56  import org.piccolo2d.util.PBounds;
57  
58  /**
59   * Venn diagram node.
60   *
61   * @param <E> value type
62   * @author  Michael Heuer
63   */
64  public class VennNode<E>
65      extends AbstractVennNode<E>
66  {
67      /** Thread pool executor service. */
68      private static final ExecutorService EXECUTOR_SERVICE = Executors.newFixedThreadPool(2);
69  
70      /** Animation length, in milliseconds, <code>2000L</code>. */
71      private static final long MS = 2000L;
72  
73      /** Area paint. */
74      private static final Paint AREA_PAINT = new Color(0, 0, 0, 0);
75  
76      /** Area stroke. */
77      private static final Stroke AREA_STROKE = null;
78  
79      /** Area stroke paint. */
80      private static final Paint AREA_STROKE_PAINT = null;
81  
82      /** Paints. */
83      private static final Paint[] PAINTS = new Color[]
84      {
85          new Color(30, 30, 30, 50),
86          new Color(5, 37, 255, 50),
87          new Color(255, 100, 5, 50),
88          new Color(11, 255, 5, 50),
89          // next twelve colors copied from set-3-12 qualitative color scheme
90          //    might need to increase alpha
91          new Color(141, 211, 199, 50),
92          new Color(255, 255, 179, 50),
93          new Color(190, 186, 218, 50),
94          new Color(251, 128, 114, 50),
95          new Color(128, 177, 211, 50),
96          new Color(253, 180, 98, 50),
97          new Color(179, 222, 105, 50),
98          new Color(252, 205, 229, 50),
99          new Color(217, 217, 217, 50),
100         new Color(188, 128, 189, 50),
101         new Color(204, 235, 197, 50),
102         new Color(255, 237, 111, 50),
103     };
104 
105     /** Stroke. */
106     private static final Stroke STROKE = new BasicStroke(0.5f);
107 
108     /** Stroke paint. */
109     private static final Paint STROKE_PAINT = new Color(20, 20, 20);
110 
111     /** Default label text. */
112     private static final String[] DEFAULT_LABEL_TEXT = new String[]
113     {
114         "First set",
115         "Second set",
116         "Third set",
117         "Fourth set",
118         "Fifth set",
119         "Sixth set",
120         "Seventh set",
121         "Eighth set",
122         "Ninth set",
123         "Tenth set",
124         "Eleventh set",
125         "Twelveth set",
126         "Thirteenth set",
127         "Fourteenth set",
128         "Fifteenth set",
129         "Sixteenth set"
130     };
131 
132     /** Path nodes. */
133     private final List<PPath> pathNodes;
134 
135     /** Labels. */
136     private final List<PText> labels;
137 
138     /** Label text. */
139     private final List<String> labelTexts;
140 
141     /** Area nodes. */
142     private final Map<ImmutableBitSet, PArea> areaNodes;
143 
144     /** Area label text, for tooltips. */
145     private final Map<ImmutableBitSet, String> areaLabelTexts;
146 
147     /** Size labels. */
148     private final Map<ImmutableBitSet, PText> sizeLabels;
149 
150     /** Venn layout. */
151     private VennLayout layout;
152 
153     /** Venn model. */
154     private final VennModel<E> model;
155 
156 
157     /**
158      * Create a new venn node with the specified model.
159      *
160      * @param model model for this venn node, must not be null
161      */
162     public VennNode(final VennModel<E> model)
163     {
164         super();
165         if (model == null)
166         {
167             throw new IllegalArgumentException("model must not be null");
168         }
169         this.model = model;
170         this.layout = new InitialLayout();
171 
172         pathNodes = new ArrayList<PPath>(this.model.size());
173         labels = new ArrayList<PText>(this.model.size());
174         labelTexts = new ArrayList<String>(this.model.size());
175         areaNodes = new HashMap<ImmutableBitSet, PArea>();
176         areaLabelTexts = new HashMap<ImmutableBitSet, String>();
177         sizeLabels = new HashMap<ImmutableBitSet, PText>();
178 
179         createNodes();
180         updateLabels();
181     }
182 
183 
184     /**
185      * Create nodes.
186      */
187     private void createNodes()
188     {
189         for (int i = 0, size = size(); i < size; i++)
190         {
191             PPath pathNode = new PPath.Double();
192             pathNode.setPaint(PAINTS[i]);
193             pathNode.setStroke(STROKE);
194             pathNode.setStrokePaint(STROKE_PAINT);
195             pathNodes.add(pathNode);
196 
197             labelTexts.add(DEFAULT_LABEL_TEXT[i]);
198 
199             PText label = new PText();
200             labels.add(label);
201         }
202 
203         // calculate the power set (note n > 30 will overflow int)
204         Set<Set<Integer>> powerSet = Sets.powerSet(range(size()));
205         for (Set<Integer> set : powerSet)
206         {
207             if (!set.isEmpty())
208             {
209                 ImmutableBitSet key = toImmutableBitSet(set);
210 
211                 PArea areaNode = new PArea();
212                 areaNode.setPaint(AREA_PAINT);
213                 areaNode.setStroke(AREA_STROKE);
214                 areaNode.setStrokePaint(AREA_STROKE_PAINT);
215                 areaNodes.put(key, areaNode);
216 
217                 PText sizeLabel = new PText();
218                 sizeLabels.put(key, sizeLabel);
219             }
220         }
221 
222         for (PText label : labels)
223         {
224             addChild(label);
225         }
226         for (PPath pathNode : pathNodes)
227         {
228             addChild(pathNode);
229         }
230         for (PText sizeLabel : sizeLabels.values())
231         {
232             addChild(sizeLabel);
233         }
234         for (PArea areaNode : areaNodes.values())
235         {
236             addChild(areaNode);
237         }
238     }
239 
240     /**
241      * Layout nodes.
242      */
243     private void layoutNodes()
244     {
245         for (int i = 0, size = size(); i < size; i++)
246         {
247             PPath pathNode = pathNodes.get(i);
248             pathNode.reset();
249             pathNode.append(layout.get(i), false);
250             PBounds pathNodeBounds = pathNode.getBoundsReference();
251 
252             PText label = labels.get(i);
253             PBounds labelBounds = label.getBoundsReference();
254             // consider layout on bottom of path if (x, y) is in bottom half of boundingRectangle
255             label.setOffset(pathNodeBounds.getX() + pathNodeBounds.getWidth() / 2.0d - labelBounds.getWidth() / 2.0d,
256                             pathNodeBounds.getY() - labelBounds.getHeight() / 2.0d - 12.0d);
257         }
258 
259         for (ImmutableBitSet key : areaNodes.keySet())
260         {
261             int first = first(key);
262             int[] additional = additional(key);
263 
264             PArea areaNode = areaNodes.get(key);
265             areaNode.reset();
266             areaNode.add(new Area(layout.get(first)));
267             // intersect with all indices in additional
268             for (int i = 0, size = additional.length; i < size; i++)
269             {
270                 areaNode.intersect(new Area(layout.get(additional[i])));
271             }
272             // subtract everything else
273             for (int i = 0, size = size(); i < size; i++)
274             {
275                 if (!key.getQuick(i))
276                 {
277                     areaNode.subtract(new Area(layout.get(i)));
278                 }
279             }
280 
281             Point2D luneCenter = layout.luneCenter(first, additional);
282             PText sizeLabel = sizeLabels.get(key);
283             PBounds sizeLabelBounds = sizeLabel.getBoundsReference();
284             // offset to lune center now
285             sizeLabel.setOffset(luneCenter.getX() - sizeLabelBounds.getWidth() / 2.0d,
286                                 luneCenter.getY() - sizeLabelBounds.getHeight() / 2.0d);
287             // delay offset to area centroids
288             EXECUTOR_SERVICE.submit(new LayoutWorker(areaNode.getAreaReference(), sizeLabel));
289         }
290     }
291 
292     @Override
293     protected void updateLabels()
294     {
295         for (int i = 0; i < size(); i++)
296         {
297             PText label = labels.get(i);
298             label.setText(buildLabel(labelTexts.get(i), model.get(i).size()));
299             label.setVisible(getDisplayLabels());
300         }
301         for (ImmutableBitSet key : areaNodes.keySet())
302         {
303             int first = first(key);
304             int[] additional = additional(key);
305             int size = model.exclusiveTo(first, additional).size();
306             boolean isEmpty = (size == 0);
307 
308             PArea areaNode = areaNodes.get(key);
309             boolean areaNodeIsEmpty = !(layout instanceof VennNode.InitialLayout) && areaNode.isEmpty();
310 
311             PText sizeLabel = sizeLabels.get(key);
312             sizeLabel.setText(String.valueOf(size));
313             sizeLabel.setVisible(getDisplaySizeLabels() && !areaNodeIsEmpty && (getDisplaySizesForEmptyAreas() || !isEmpty));
314 
315             areaLabelTexts.put(key, buildAreaLabel(first, additional));
316         }
317     }
318 
319     /**
320      * Return the size of this venn node.
321      *
322      * @return the size of this venn node
323      */
324     public final int size()
325     {
326         return model.size();
327     }
328 
329     /**
330      * Return the layout for this venn node.  The layout will not be null.
331      *
332      * @return the layout for this venn node
333      */
334     public final VennLayout getLayout()
335     {
336         return layout;
337     }
338 
339     /**
340      * Set the layout for this venn node to <code>layout</code>.
341      *
342      * <p>This is a bound property.</p>
343      *
344      * @param layout layout for this venn node, must not be null
345      */
346     public final void setLayout(final VennLayout layout)
347     {
348         if (layout == null)
349         {
350             throw new IllegalArgumentException("layout must not be null");
351         }
352         VennLayout oldLayout = this.layout;
353         this.layout = layout;
354         firePropertyChange(-1, "layout", oldLayout, this.layout);
355 
356         SwingUtilities.invokeLater(new Runnable()
357             {
358                 @Override
359                 public void run()
360                 {
361                     layoutNodes();
362                     updateLabels();
363                 }
364             });
365     }
366 
367     /**
368      * Return the model for this venn node.  The model will not be null.
369      *
370      * @return the model for this venn node
371      */
372     public final VennModel<E> getModel()
373     {
374         return model;
375     }
376 
377     /**
378      * Return the path node for the set at the specified index.
379      *
380      * @param index index
381      * @return the path node for the set at the specified index
382      * @throws IndexOutOfBoundsException if <code>index</code> is out of bounds
383      */
384     public final PPath getPath(final int index)
385     {
386         return pathNodes.get(index);
387     }
388 
389     /**
390      * Return the label text for the set at the specified index.  Defaults to {@link #DEFAULT_LABEL_TEXT}<code>.get(index)</code>.
391      *
392      * @param index index
393      * @return the label text for the set at the specified index
394      * @throws IndexOutOfBoundsException if <code>index</code> is out of bounds
395      */
396     public final String getLabelText(final int index)
397     {
398         return labelTexts.get(index);
399     }
400 
401     /**
402      * Set the label text for the set at the specified index to <code>labelText</code>.
403      *
404      * <p>This is a bound property.</p>
405      *
406      * @param index index
407      * @param labelText label text for the set at the specified index
408      * @throws IndexOutOfBoundsException if <code>index</code> is out of bounds
409      */
410     public final void setLabelText(final int index, final String labelText)
411     {
412         String oldLabelText = labelTexts.get(index);
413         labelTexts.set(index, labelText);
414         firePropertyChange(-1, "labelTexts", oldLabelText, labelTexts.get(index));
415         updateLabels();
416     }
417 
418     /**
419      * Return the label for the set at the specified index.  The text for the returned PText
420      * should not be changed, as the text is synchronized to the venn model backing this venn
421      * diagram.  Use methods {@link #setLabelText(int, String)} and {@link #setDisplaySizes(boolean)}
422      * to set the label text and whether to display sizes respectively.
423      *
424      * @return the label for the set at the specified index
425      */
426     public final PText getLabel(final int index)
427     {
428         return labels.get(index);
429     }
430 
431     /**
432      * Return the area node for the intersecting area defined by the specified indices.
433      *
434      * @param index first index
435      * @param additional variable number of additional indices, if any
436      * @return the area node for the intersecting area defined by the specified indices
437      * @throws IndexOutOfBoundsException if <code>index</code> or any of <code>additional</code>
438      *    are out of bounds, or if too many indices are specified
439      */
440     public final PArea getArea(final int index, final int... additional)
441     {
442         checkIndices(index, additional);
443         return areaNodes.get(toImmutableBitSet(index, additional));
444     }
445 
446     /**
447      * Return the area label text for the intersecting area defined by the specified indices.
448      *
449      * @param index first index
450      * @param additional variable number of additional indices, if any
451      * @return the area label text for the intersecting area defined by the specified indices
452      * @throws IndexOutOfBoundsException if <code>index</code> or any of <code>additional</code>
453      *    are out of bounds, or if too many indices are specified
454      */
455     public final String getAreaLabelText(final int index, final int... additional)
456     {
457         checkIndices(index, additional);
458         return areaLabelTexts.get(toImmutableBitSet(index, additional));
459     }
460 
461     /**
462      * Return the size label for the intersecting area defined by the specified indices.
463      *
464      * @param index first index
465      * @param additional variable number of additional indices, if any
466      * @return the size label for the intersecting area defined by the specified indices
467      * @throws IndexOutOfBoundsException if <code>index</code> or any of <code>additional</code>
468      *    are out of bounds, or if too many indices are specified
469      */
470     public final PText getSizeLabel(final int index, final int... additional)
471     {
472         checkIndices(index, additional);
473         return sizeLabels.get(toImmutableBitSet(index, additional));
474     }
475 
476     @Override
477     public Iterable<PText> labels()
478     {
479         return labels;
480     }
481 
482     @Override
483     public Iterable<PNode> nodes()
484     {
485         List<PNode> nodes = new ArrayList<PNode>(pathNodes.size() + areaNodes.size());
486         nodes.addAll(pathNodes);
487         nodes.addAll(areaNodes.values());
488         return nodes;
489     }
490 
491     @Override
492     public PText labelForNode(final PNode node)
493     {
494         if (node instanceof PPath)
495         {
496             PPath pathNode = (PPath) node;
497             int index = pathNodes.indexOf(pathNode);
498             return labels.get(index);
499         }
500         return null;
501     }
502 
503     @Override
504     public String labelTextForNode(final PNode node)
505     {
506         if (node instanceof PPath)
507         {
508             PPath pathNode = (PPath) node;
509             int index = pathNodes.indexOf(pathNode);
510             return labelTexts.get(index);
511         }
512         else if (node instanceof PArea)
513         {
514             PArea areaNode = (PArea) node;
515             for (Map.Entry<ImmutableBitSet, PArea> entry : areaNodes.entrySet())
516             {
517                 if (entry.getValue().equals(areaNode))
518                 {
519                     return areaLabelTexts.get(entry.getKey());
520                 }
521             }
522         }
523         return null;
524     }
525 
526     @Override
527     public Iterable<PText> sizeLabels()
528     {
529         return sizeLabels.values();
530     }
531 
532     @Override
533     public Set<E> viewForNode(final PNode node)
534     {
535         if (node instanceof PPath)
536         {
537             PPath pathNode = (PPath) node;
538             int index = pathNodes.indexOf(pathNode);
539             return model.get(index);
540         }
541         else if (node instanceof PArea)
542         {
543             PArea areaNode = (PArea) node;
544             for (Map.Entry<ImmutableBitSet, PArea> entry : areaNodes.entrySet())
545             {
546                 if (entry.getValue().equals(areaNode))
547                 {
548                     ImmutableBitSet key = entry.getKey();
549                     return model.exclusiveTo(first(key), additional(key));
550                 }
551             }
552         }
553         return null;
554     }
555 
556     /**
557      * Build and return area label text.
558      *
559      * @param index index
560      * @param additional variable number of additional indices
561      * @throws IndexOutOfBoundsException if <code>index</code> or any of <code>additional</code>
562      *    are out of bounds, or if too many indices are specified
563      */
564     protected final String buildAreaLabel(final int index, final int... additional)
565     {
566         checkIndices(index, additional);
567         StringBuilder sb = new StringBuilder();
568         sb.append(labelTexts.get(index));
569         if (additional.length > 0) {
570             for (int i = 0, size = additional.length - 1; i < size; i++)
571             {
572                 sb.append(", ");
573                 sb.append(labelTexts.get(additional[i]));
574             }
575             sb.append(" and ");
576             sb.append(labelTexts.get(additional[Math.max(0, additional.length - 1)]));
577         }
578         sb.append(" only");
579         return sb.toString();
580     }
581 
582     /**
583      * Check the specified indices are valid.
584      *
585      * @param index index
586      * @param additional variable number of additional indices
587      * @throws IndexOutOfBoundsException if <code>index</code> or any of <code>additional</code>
588      *    are out of bounds, or if too many indices are specified
589      */
590     private void checkIndices(final int index, final int... additional)
591     {
592         int maxIndex = size() - 1;
593         if (index < 0 || index > maxIndex)
594         {
595             throw new IndexOutOfBoundsException("index out of bounds");
596         }
597         if (additional != null && additional.length > 0)
598         {
599             if (additional.length > maxIndex)
600             {
601                 throw new IndexOutOfBoundsException("too many indices provided");
602             }
603             for (int i = 0, j = additional.length; i < j; i++)
604             {
605                 if (additional[i] < 0 || additional[i] > maxIndex)
606                 {
607                     throw new IndexOutOfBoundsException("additional index [" + i + "] out of bounds");
608                 }
609             }
610         }
611     }
612 
613     /**
614      * Return an immutable set of the integers between <code>0</code> and <code>n</code>, exclusive.
615      *
616      * @param n n
617      * @return an immutable set of integers between <code>0</code> and <code>n</code>, exclusive
618      */
619     static ImmutableSet<Integer> range(final int n)
620     {
621         Set<Integer> range = Sets.newHashSet();
622         for (int i = 0; i < n; i++)
623         {
624             range.add(Integer.valueOf(i));
625         }
626         return ImmutableSet.copyOf(range);
627     }
628 
629     /**
630      * Return the first index set to true in the specified bit set.
631      *
632      * @param bitSet bit set
633      * @return the first index set to true in the specified bit set
634      *    or <code>-1</code> if no bits in the specified bit set are set to true
635      */
636     static int first(final ImmutableBitSet bitSet)
637     {
638         return (int) bitSet.nextSetBit(0L);
639     }
640 
641     /**
642      * Return the additional indices set to true in the specified bit set.
643      *
644      * @param bitSet bit set
645      * @return the additional indices set to true in the specified bit set
646      *    or an empty <code>int[]</code> if only zero or one bits are set to true
647      *    the specified bit set
648      */
649     static int[] additional(final ImmutableBitSet bitSet)
650     {
651         int[] additional = new int[Math.max(0, (int) bitSet.cardinality() - 1)];
652         int index = 0;
653         long first = bitSet.nextSetBit(0);
654         for (long value = bitSet.nextSetBit(first + 1); value >= 0L; value = bitSet.nextSetBit(value + 1))
655         {
656             additional[index] = (int) value;
657             index++;
658         }
659         return additional;
660     }
661 
662     /**
663      * Return the first int value in the specified set of values.
664      *
665      * @param values set of values
666      * @return the first int value int the specified set of values
667      */
668     static int first(final Set<Integer> values)
669     {
670         if (values.isEmpty())
671         {
672             return -1;
673         }
674         return values.iterator().next().intValue();
675     }
676 
677     /**
678      * Return the additional int values in the specified set of values.
679      *
680      * @param values set of values
681      * @return the additional it values in the specified set of values
682      */
683     static int[] additional(final Set<Integer> values)
684     {
685         int[] additional = new int[Math.max(0, values.size() - 1)];
686         int index = -1;
687         for (Integer value : values)
688         {
689             if (index >= 0)
690             {
691                 additional[index] = value.intValue();
692             }
693             index++;
694         }
695         return additional;
696     }
697 
698     // copied from VennLayoutUtils.java here to keep package-private visibility
699     /**
700      * Create and return a new immutable bit set with the specified bits set to true.
701      *
702      * @param index first index to set to true
703      * @param additional variable number of additional indices to set to true, if any
704      * @return a new immutable bit set with the specified bits set to true
705      */
706     static ImmutableBitSet toImmutableBitSet(final int index, final int... additional)
707     {
708         int size = 1 + ((additional == null) ? 0 : additional.length);
709         MutableBitSet mutableBitSet = new MutableBitSet(size);
710         mutableBitSet.set(index);
711         if (additional != null)
712         {
713             for (int i = 0; i < additional.length; i++)
714             {
715                 mutableBitSet.set(additional[i]);
716             }
717         }
718         mutableBitSet.trimTrailingZeros();
719         return mutableBitSet.immutableCopy();
720     }
721 
722     /**
723      * Create and return a new immutable bit set with the specified bits set to true.
724      *
725      * @param indices set of indices to set to true, must not be null and must not be empty
726      * @return a new immutable bit set with the specified bits set to true
727      */
728     static ImmutableBitSet toImmutableBitSet(final Set<Integer> indices)
729     {
730         if (indices == null)
731         {
732             throw new IllegalArgumentException("indices must not be null");
733         }
734         if (indices.isEmpty())
735         {
736             throw new IllegalArgumentException("indices must not be empty");
737         }
738         MutableBitSet mutableBitSet = new MutableBitSet(indices.size());
739         for (Integer index : indices)
740         {
741             mutableBitSet.set(index);
742         }
743         mutableBitSet.trimTrailingZeros();
744         return mutableBitSet.immutableCopy();
745     }
746 
747     /**
748      * Intial layout.
749      */
750     private final class InitialLayout implements VennLayout {
751         /** Offscreen left. */
752         private final Point2D offscreenLeft = new Point2D.Double(-10000.0d, 0.0d);
753 
754         /** Empty. */
755         private final Rectangle2D empty = new Rectangle2D.Double(-10000.0d, 0.0d, 0.0d, 0.0d);
756 
757 
758         @Override
759         public int size()
760         {
761             return VennNode.this.size();
762         }
763 
764         @Override
765         public Shape get(final int index)
766         {
767             if (index < 0 || index >= size())
768             {
769                 throw new IndexOutOfBoundsException("index " + index + " out of bounds");
770             }
771             return empty;
772         }
773 
774         @Override
775         public Point2D luneCenter(final int index, final int... additional)
776         {
777             checkIndices(index, additional);
778             return offscreenLeft;
779         }
780 
781         @Override
782         public Rectangle2D boundingRectangle()
783         {
784             return empty;
785         }
786     }
787 
788     // copied from QuaternaryVennNode.java
789     /**
790      * Layout worker.
791      */
792     private final class LayoutWorker
793         extends SwingWorker<Point2D, Object>
794     {
795         /** Area for this layout worker. */
796         private Area area;
797 
798         /** Size label for this layout worker. */
799         private PText size;
800 
801 
802         /**
803          * Create a new layout worker for the specified area and size label.
804          *
805          * @param area area
806          * @param size size label
807          */
808         private LayoutWorker(final Area area, final PText size)
809         {
810             this.area = area;
811             this.size = size;
812         }
813 
814 
815         @Override
816         public Point2D doInBackground()
817         {
818             return Centers.centroidOf(area);
819         }
820 
821         @Override
822         protected void done()
823         {
824             try
825             {
826                 Rectangle2D bounds = size.getFullBoundsReference();
827                 Point2D centroid = get();
828                 size.animateToPositionScaleRotation(centroid.getX() - (bounds.getWidth() / 2.0d),
829                                                     centroid.getY() - (bounds.getHeight() / 2.0d), 1.0d, 0.0d, MS);
830             }
831             catch (Exception e)
832             {
833                 // ignore
834             }
835         }
836     }
837 }