001/* An icon that renders the value of an attribute of the container.
002
003 Copyright (c) 1999-2016 The Regents of the University of California.
004 All rights reserved.
005 Permission is hereby granted, without written agreement and without
006 license or royalty fees, to use, copy, modify, and distribute this
007 software and its documentation for any purpose, provided that the above
008 copyright notice and the following two paragraphs appear in all copies
009 of this software.
010
011 IN NO EVENT SHALL THE UNIVERSITY OF CALIFORNIA BE LIABLE TO ANY PARTY
012 FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES
013 ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN IF
014 THE UNIVERSITY OF CALIFORNIA HAS BEEN ADVISED OF THE POSSIBILITY OF
015 SUCH DAMAGE.
016
017 THE UNIVERSITY OF CALIFORNIA SPECIFICALLY DISCLAIMS ANY WARRANTIES,
018 INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
019 MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE
020 PROVIDED HEREUNDER IS ON AN "AS IS" BASIS, AND THE UNIVERSITY OF
021 CALIFORNIA HAS NO OBLIGATION TO PROVIDE MAINTENANCE, SUPPORT, UPDATES,
022 ENHANCEMENTS, OR MODIFICATIONS.
023
024 PT_COPYRIGHT_VERSION_2
025 COPYRIGHTENDKEY
026
027 */
028package ptolemy.vergil.icon;
029
030import java.awt.Color;
031import java.awt.Font;
032import java.awt.geom.Rectangle2D;
033import java.util.Iterator;
034import java.util.Random;
035
036import javax.swing.SwingConstants;
037
038import diva.canvas.CompositeFigure;
039import diva.canvas.Figure;
040import diva.canvas.toolbox.BasicRectangle;
041import diva.canvas.toolbox.LabelFigure;
042import ptolemy.actor.gui.ColorAttribute;
043import ptolemy.data.ArrayToken;
044import ptolemy.data.RecordToken;
045import ptolemy.data.StringToken;
046import ptolemy.data.Token;
047import ptolemy.data.expr.Parameter;
048import ptolemy.data.expr.StringParameter;
049import ptolemy.data.expr.Variable;
050import ptolemy.data.type.ArrayType;
051import ptolemy.data.type.BaseType;
052import ptolemy.kernel.util.Attribute;
053import ptolemy.kernel.util.IllegalActionException;
054import ptolemy.kernel.util.NameDuplicationException;
055import ptolemy.kernel.util.NamedObj;
056import ptolemy.kernel.util.Settable;
057
058///////////////////////////////////////////////////////////////////
059//// TableIcon
060
061/**
062 An icon that displays the value of a variable of the container in a
063 table. The attribute is assumed to be an instance of Variable, and its name
064 is given by the parameter <i>variableName</i>. Its value must be an
065 array of records.  A subset of the fields in the records given by
066 the <i>fields</i> parameter is displayed in the icon.
067
068 @author Edward A. Lee
069 @version $Id$
070 @since Ptolemy II 8.0
071 @Pt.ProposedRating Yellow (eal)
072 @Pt.AcceptedRating Red (johnr)
073 */
074public class TableIcon extends DynamicEditorIcon {
075    /** Create a new icon with the given name in the given container.
076     *  The container is required to implement Settable, or an exception
077     *  will be thrown.
078     *  @param container The container for this attribute.
079     *  @param name The name of this attribute.
080     *  @exception IllegalActionException If thrown by the parent
081     *  class or while setting an attribute
082     *  @exception NameDuplicationException If the name coincides with
083     *   an attribute already in the container.
084     */
085    public TableIcon(NamedObj container, String name)
086            throws NameDuplicationException, IllegalActionException {
087        super(container, name);
088
089        variableName = new StringParameter(this, "variableName");
090
091        boxColor = new ColorAttribute(this, "boxColor");
092        boxColor.setExpression("{1.0, 1.0, 1.0, 1.0}");
093
094        Variable UNBOUNDED = new Variable(this, "UNBOUNDED");
095        UNBOUNDED.setVisibility(Settable.NONE);
096        UNBOUNDED.setExpression("0");
097
098        maxRows = new Parameter(this, "maxRows");
099        maxRows.setTypeEquals(BaseType.INT);
100        maxRows.setExpression("UNBOUNDED");
101
102        Variable ALL = new Variable(this, "ALL");
103        ALL.setVisibility(Settable.NONE);
104        Token emptyStringArray = new ArrayToken(BaseType.STRING);
105        ALL.setToken(emptyStringArray);
106
107        fields = new Parameter(this, "fields");
108        fields.setTypeEquals(new ArrayType(BaseType.STRING));
109        fields.setExpression("ALL");
110
111        colorKey = new StringParameter(this, "colorKey");
112    }
113
114    ///////////////////////////////////////////////////////////////////
115    ////                         parameters                        ////
116
117    /** Color of the box. This defaults to white. */
118    public ColorAttribute boxColor;
119
120    /** A column name to use as a color key. If this string is
121     *  non-empty, then it specifies a column name that is used
122     *  to determine a color for each row. The value in that
123     *  row and column determines the color via a hash function,
124     *  so that if two rows are identical in that column, then
125     *  they are also identical in color.  This is a string that
126     *  defaults to empty, indicating that all rows should
127     *  be displayed in black.
128     */
129    public StringParameter colorKey;
130
131    /** The fields to display in the table.
132     *  This is an array of strings specifying the field
133     *  names to display. It defaults to ALL, which indicates
134     *  that all fields should be displayed.
135     */
136    public Parameter fields;
137
138    /** The maximum number of rows to display. This is an integer, with
139     *  default value UNBOUNDED.
140     */
141    public Parameter maxRows;
142
143    /** The name of the variable in the container
144     *  whose value should be displayed in the icon. The variable
145     *  value must be an array of records. This is a string that
146     *  defaults to the empty string.
147     */
148    public StringParameter variableName;
149
150    ///////////////////////////////////////////////////////////////////
151    ////                         public methods                    ////
152
153    /** Create a new background figure.  This overrides the base class
154     *  to draw a box around the value display, where the width of the
155     *  box depends on the value.
156     *  @return A new figure.
157     */
158    @Override
159    public Figure createBackgroundFigure() {
160        NamedObj container = getContainer();
161        CompositeFigure result = new CompositeFigure();
162        if (container != null) {
163            try {
164                ArrayToken fieldsValue = (ArrayToken) fields.getToken();
165                Attribute associatedAttribute = container
166                        .getAttribute(variableName.getExpression());
167                if (associatedAttribute instanceof Variable) {
168                    Token value = ((Variable) associatedAttribute).getToken();
169                    if (value instanceof ArrayToken) {
170                        // Find the number of rows and columns.
171                        int numRows = ((ArrayToken) value).length();
172                        int numColumns = fieldsValue.length();
173                        if (numColumns == 0) {
174                            // All columns should be included.
175                            // Make a pass to figure out how many that is.
176                            for (int i = 0; i < numRows; i++) {
177                                Token row = ((ArrayToken) value).getElement(i);
178                                if (row instanceof RecordToken) {
179                                    int rowWidth = ((RecordToken) row)
180                                            .labelSet().size();
181                                    if (rowWidth > numColumns) {
182                                        numColumns = rowWidth;
183                                    }
184                                }
185                            }
186                        }
187
188                        // Find the width of each column and the height of each row.
189                        // All rows are the same height, but column widths can vary.
190                        double rowHeight = 0.0;
191                        double columnWidth[] = new double[numColumns];
192                        for (int i = 1; i < numColumns; i++) {
193                            columnWidth[i] = 0.0;
194                        }
195                        LabelFigure tableElement[][] = new LabelFigure[numRows][numColumns];
196                        // Iterate over rows.
197                        for (int i = 0; i < numRows; i++) {
198                            Token row = ((ArrayToken) value).getElement(i);
199                            if (row instanceof RecordToken) {
200                                if (fieldsValue.length() == 0) {
201                                    // Display all fields.
202                                    Iterator labelSet = ((RecordToken) row)
203                                            .labelSet().iterator();
204                                    int j = 0;
205                                    while (labelSet.hasNext()) {
206                                        String column = (String) labelSet
207                                                .next();
208                                        tableElement[i][j] = _labelFigure(
209                                                (RecordToken) row, column);
210                                        Rectangle2D bounds = tableElement[i][j]
211                                                .getBounds();
212                                        double width = bounds.getWidth();
213                                        if (width > columnWidth[j]) {
214                                            columnWidth[j] = width;
215                                        }
216                                        double height = bounds.getHeight();
217                                        if (height > rowHeight) {
218                                            rowHeight = height;
219                                        }
220                                        j++;
221                                    }
222                                } else {
223                                    // Display specified fields.
224                                    for (int j = 0; j < fieldsValue
225                                            .length(); j++) {
226                                        if (j >= numColumns) {
227                                            break;
228                                        }
229                                        String column = ((StringToken) fieldsValue
230                                                .getElement(j)).stringValue();
231                                        tableElement[i][j] = _labelFigure(
232                                                (RecordToken) row, column);
233                                        Rectangle2D bounds = tableElement[i][j]
234                                                .getBounds();
235                                        double width = bounds.getWidth();
236                                        if (width > columnWidth[j]) {
237                                            columnWidth[j] = width;
238                                        }
239                                        double height = bounds.getHeight();
240                                        if (height > rowHeight) {
241                                            rowHeight = height;
242                                        }
243                                    }
244                                }
245                            }
246                        }
247
248                        // Now make a pass to position and add all the figures.
249                        double rowPosition = _VERTICAL_PADDING;
250                        // Iterate over rows.
251                        for (int i = 0; i < numRows; i++) {
252                            Token row = ((ArrayToken) value).getElement(i);
253                            if (row instanceof RecordToken) {
254                                if (fieldsValue.length() == 0) {
255                                    // Display all fields.
256                                    Iterator labelSet = ((RecordToken) row)
257                                            .labelSet().iterator();
258                                    int j = 0;
259                                    double columnPosition = _HORIZONTAL_PADDING;
260                                    while (labelSet.hasNext()) {
261                                        /*String column = (String) */labelSet
262                                                .next();
263                                        tableElement[i][j].translateTo(
264                                                columnPosition, rowPosition);
265                                        result.add(tableElement[i][j]);
266                                        columnPosition += columnWidth[j]
267                                                + _HORIZONTAL_PADDING;
268                                        j++;
269                                    }
270                                } else {
271                                    // Display specified fields.
272                                    double columnPosition = _HORIZONTAL_PADDING;
273                                    for (int j = 0; j < fieldsValue
274                                            .length(); j++) {
275                                        // String column = ((StringToken)fieldsValue.getElement(j)).stringValue();
276                                        tableElement[i][j].translateTo(
277                                                columnPosition, rowPosition);
278                                        result.add(tableElement[i][j]);
279                                        columnPosition += columnWidth[j]
280                                                + _HORIZONTAL_PADDING;
281                                    }
282                                }
283                            }
284                            rowPosition += rowHeight + _VERTICAL_PADDING;
285                        }
286                    }
287                }
288            } catch (IllegalActionException e) {
289                // Stick the error message in the icon.
290                result.add(new LabelFigure(e.getMessage()));
291            }
292        }
293        // Now put a box around it all.
294        Rectangle2D bounds = result.getBounds();
295        // Double the padding below to allow for both sides.
296        double width = Math.floor(bounds.getWidth()) + _HORIZONTAL_PADDING * 2;
297        double height = Math.floor(bounds.getHeight()) + _VERTICAL_PADDING * 2;
298        Figure rectangle = new BasicRectangle(0, 0, width, height,
299                boxColor.asColor(), 1);
300        CompositeFigure finalResult = new CompositeFigure(rectangle);
301        finalResult.add(result);
302        return finalResult;
303    }
304
305    ///////////////////////////////////////////////////////////////////
306    ////                         protected members                 ////
307
308    /** The font used. */
309    protected static final Font _labelFont = new Font("SansSerif", Font.PLAIN,
310            12);
311
312    /** The amount of padding to use around the edges. */
313    protected static final double _HORIZONTAL_PADDING = 5.0;
314
315    /** The amount of padding to use around the edges. */
316    protected static final double _VERTICAL_PADDING = 3.0;
317
318    ///////////////////////////////////////////////////////////////////
319    ////                         private methods                   ////
320
321    /** Return a label figure for the specified token.
322     *  @param row The row.
323     *  @param column The column.
324     *  @return A label figure.
325     */
326    private LabelFigure _labelFigure(RecordToken row, String column) {
327        Token token = row.get(column);
328        String label = "";
329        if (token instanceof StringToken) {
330            label = ((StringToken) token).stringValue();
331        } else if (token != null) {
332            label = token.toString();
333        }
334        Color color = Color.black;
335        try {
336            if (!colorKey.stringValue().equals("")) {
337                Token colorToken = row.get(colorKey.stringValue());
338                if (colorToken != null) {
339                    color = _uniqueColor(colorToken.toString());
340                }
341            }
342        } catch (IllegalActionException e) {
343            // Ignore. Use black.
344        }
345
346        LabelFigure tableElement = new LabelFigure(label, _labelFont, 1.0,
347                SwingConstants.NORTH_WEST, color);
348        return tableElement;
349    }
350
351    private Color _uniqueColor(Object object) {
352        // Get a color from the hash code. We will use
353        // the low order 24 bits only.
354        int hashCode = object.hashCode();
355        // Use the code as a seed for a random number generator.
356        // FindBugs: [H B BC] Random object created and used only once [DMI_RANDOM_USED_ONLY_ONCE]
357        // Actually this is the intend since you want a unique color for
358        // each specific object (hence it can't be completely random).
359        int code = new Random(hashCode).nextInt();
360        float red = (code >> 16 & 0xff) / 256.0f;
361        float green = (code >> 8 & 0xff) / 256.0f;
362        float blue = (code & 0xff) / 256.0f;
363        // Make sure the color is at least as dark as close to a pure red, green, or blue.
364        // This means that the magnitude of the r,g,b vector is no greater than 1.0.
365        float magnitude = (float) Math
366                .sqrt(red * red + green * green + blue * blue);
367        if (magnitude < 0.8f) {
368            magnitude = 0.8f;
369        }
370
371        Color result = new Color(red / magnitude, green / magnitude,
372                blue / magnitude);
373        return result;
374    }
375}