Coverage Report - org.geotoolkit.gui.swing.image.IIOMetadataPanel
 
Classes in this File Line Coverage Branch Coverage Complexity
IIOMetadataPanel
37 %
53/140
6 %
4/66
3,8
IIOMetadataPanel$Controller
53 %
16/30
37 %
3/8
3,8
 
 1  
 /*
 2  
  *    Geotoolkit.org - An Open Source Java GIS Toolkit
 3  
  *    http://www.geotoolkit.org
 4  
  *
 5  
  *    (C) 2009-2012, Open Source Geospatial Foundation (OSGeo)
 6  
  *    (C) 2009-2012, Geomatys
 7  
  *
 8  
  *    This library is free software; you can redistribute it and/or
 9  
  *    modify it under the terms of the GNU Lesser General Public
 10  
  *    License as published by the Free Software Foundation;
 11  
  *    version 2.1 of the License.
 12  
  *
 13  
  *    This library is distributed in the hope that it will be useful,
 14  
  *    but WITHOUT ANY WARRANTY; without even the implied warranty of
 15  
  *    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 16  
  *    Lesser General Public License for more details.
 17  
  */
 18  
 package org.geotoolkit.gui.swing.image;
 19  
 
 20  
 import java.util.Map;
 21  
 import java.util.List;
 22  
 import java.util.Locale;
 23  
 import java.util.ArrayList;
 24  
 import java.util.LinkedHashMap;
 25  
 import java.util.IdentityHashMap;
 26  
 import java.awt.*;
 27  
 import java.awt.event.ActionEvent;
 28  
 import java.awt.event.ActionListener;
 29  
 import javax.swing.*;
 30  
 import javax.swing.tree.TreePath;
 31  
 import javax.swing.event.TreeSelectionEvent;
 32  
 import javax.swing.event.TreeSelectionListener;
 33  
 import javax.imageio.metadata.IIOMetadata;
 34  
 import javax.imageio.metadata.IIOMetadataFormat;
 35  
 import javax.imageio.metadata.IIOMetadataFormatImpl;
 36  
 
 37  
 import org.jdesktop.swingx.JXTreeTable;
 38  
 
 39  
 import org.geotoolkit.resources.Vocabulary;
 40  
 import org.geotoolkit.util.NumberRange;
 41  
 import org.geotoolkit.util.converter.Classes;
 42  
 import org.geotoolkit.image.io.metadata.MetadataTreeNode;
 43  
 import org.geotoolkit.image.io.metadata.MetadataTreeTable;
 44  
 import org.geotoolkit.image.io.metadata.SpatialMetadataFormat;
 45  
 import org.geotoolkit.internal.swing.ComboBoxRenderer;
 46  
 
 47  
 
 48  
 /**
 49  
  * A panel showing the content of an {@link IIOMetadata} instance. This panel contains three parts:
 50  
  * <p>
 51  
  * <ul>
 52  
  *  <li>At the top, a field allowing to select which metadata to display:
 53  
  *   <ul>
 54  
  *    <li>The metadata format, typically
 55  
  *        {@value org.geotoolkit.image.io.metadata.SpatialMetadataFormat#FORMAT_NAME} or
 56  
  *        {@value javax.imageio.metadata.IIOMetadataFormatImpl#standardMetadataFormatName}.
 57  
  *    </li>
 58  
  *    <li>The metadata part to display: <cite>stream</cite> metadata or one of the
 59  
  *        <cite>image</cite> metadata.</li>
 60  
  *   </ul>
 61  
  *  </li>
 62  
  *  <li>At the center, the metadata as a {@linkplain JXTreeTable tree table}.
 63  
  *      The columns are documented in {@link MetadataTreeTable} javadoc.</li>
 64  
  *  <li>At the bottom, a description of the currently selected metadata node.</li>
 65  
  * </ul>
 66  
  * <p>
 67  
  * Most columns are hiden by default. The initial view shows only (<var>name</var>, <var>value</var>) pairs in
 68  
  * the {@link IIOMetadata} case, or (<var>name</var>, <var>type</var>) pairs in the {@link IIOMetadataFormat}
 69  
  * case. Users can make additional columns visible by clicking on the icon in the upper-right corner.
 70  
  * <p>
 71  
  * This class can be used in two ways (choose only one):
 72  
  * <p>
 73  
  * <ul>
 74  
  *   <li>For displaying the structure of {@link IIOMetadataFormat} instances without data,
 75  
  *     invoke {@link #addMetadataFormat addMetadataFormat(...)}.</li>
 76  
  *
 77  
  *   <li>For displaying the actual content of {@link IIOMetadata} instances, invoke
 78  
  *     {@link #addMetadata addMetadata(...)}.</li>
 79  
  * </ul>
 80  
  *
 81  
  * <table cellspacing="24" cellpadding="12" align="center"><tr valign="top">
 82  
  * <td width="500" bgcolor="lightblue">
 83  
  * {@section Demo}
 84  
  * To try this component in your browser, see the
 85  
  * <a href="http://www.geotoolkit.org/demos/geotk-simples/applet/IIOMetadataPanel.html">demonstration applet</a>.
 86  
  * </td></tr></table>
 87  
  *
 88  
  * @author Martin Desruisseaux (Geomatys)
 89  
  * @version 3.12
 90  
  *
 91  
  * @see MetadataTreeTable
 92  
  *
 93  
  * @since 3.05
 94  
  * @module
 95  
  */
 96  
 @SuppressWarnings("serial")
 97  
 public class IIOMetadataPanel extends JComponent {
 98  
     /**
 99  
      * The choices of metadata format. Typical choices are
 100  
      * {@value org.geotoolkit.image.io.metadata.SpatialMetadataFormat#FORMAT_NAME} and
 101  
      * {@value javax.imageio.metadata.IIOMetadataFormatImpl#standardMetadataFormatName}.
 102  
      */
 103  
     private final DefaultComboBoxModel formatChoices;
 104  
 
 105  
     /**
 106  
      * The properties of the currently selected metadata node.
 107  
      */
 108  
     private final JLabel description, validValues;
 109  
 
 110  
     /**
 111  
      * The unique instance of the set of listeners which is associated to this panel.
 112  
      */
 113  
     private final Controller controller;
 114  
 
 115  
     /**
 116  
      * The name of images for each index. This is an undocumented feature for now.
 117  
      */
 118  
     transient List<String> imageNames;
 119  
 
 120  
     /**
 121  
      * Creates a panel with no initial metadata. One of the {@code addXXXMetadata} or
 122  
      * {@code addXXXMetadataFormat} methods should be invoked in order to display a content.
 123  
      */
 124  2
     public IIOMetadataPanel() {
 125  2
         setLayout(new BorderLayout());
 126  
         // If the preferred width is modified, consider updating the
 127  
         // preferred column width in IIOMetadataTreeTable constructor.
 128  2
         setPreferredSize(new Dimension(500, 400));
 129  2
         final JComponent tables = new JPanel(new CardLayout());
 130  2
         add(tables, BorderLayout.CENTER);
 131  2
         final Vocabulary resources = Vocabulary.getResources(getLocale());
 132  
         /*
 133  
          * Add the control button on top of the metadata table.
 134  
          */
 135  2
         formatChoices = new DefaultComboBoxModel();
 136  2
         final JComboBox formats = new JComboBox(formatChoices);
 137  2
         ComboBoxRenderer.install(formats);
 138  2
         formats.setName("Formats");
 139  
         if (true) {
 140  2
             final JPanel controls = new JPanel(new GridBagLayout());
 141  2
             final GridBagConstraints c = new GridBagConstraints();
 142  2
             final Insets ci = c.insets;
 143  2
             ci.left = 12;
 144  2
             c.gridy=0; ci.top=6; ci.bottom=0;
 145  2
             c.gridx=0; c.weightx=0; c.anchor=GridBagConstraints.WEST;
 146  2
             controls.add(label(resources, Vocabulary.Keys.FORMAT, formats), c);
 147  2
             c.insets.right = 12; c.insets.left = 0;
 148  2
             c.gridx=1; c.weightx=1; c.fill=GridBagConstraints.BOTH;
 149  2
             controls.add(formats, c);
 150  2
             add(controls, BorderLayout.NORTH);
 151  2
             controls.setOpaque(false);
 152  
         }
 153  
         /*
 154  
          * Add the section for metadata properties.
 155  
          */
 156  
         if (true) {
 157  2
             final JPanel properties = new JPanel(new GridBagLayout());
 158  2
             final GridBagConstraints c = new GridBagConstraints();
 159  2
             final Insets ci = c.insets;
 160  2
             c.gridx=1; c.weightx=1; c.anchor=GridBagConstraints.WEST;
 161  2
             c.gridy=0; ci.top=3; ci.bottom=0; properties.add(description = new JLabel(), c);
 162  2
             c.gridy++; ci.top=0; ci.bottom=3; properties.add(validValues = new JLabel(), c);
 163  2
             ci.left = 6;
 164  2
             c.gridx=0; c.weightx=0; ci.right=9;
 165  2
             c.gridy=0; ci.top=3; ci.bottom=0; properties.add(label(resources, Vocabulary.Keys.DESCRIPTION,  description), c);
 166  2
             c.gridy++; ci.top=0; ci.bottom=3; properties.add(label(resources, Vocabulary.Keys.VALID_VALUES, validValues), c);
 167  2
             add(properties, BorderLayout.SOUTH);
 168  2
             properties.setOpaque(false);
 169  
         }
 170  
         /*
 171  
          * Plug the listeners.
 172  
          */
 173  2
         controller = new Controller(tables);
 174  2
         formats.addActionListener(controller);
 175  2
     }
 176  
 
 177  
     /**
 178  
      * Creates a new label for the given target component.
 179  
      */
 180  
     private static JLabel label(final Vocabulary resources, final int key, final JComponent target) {
 181  6
         final JLabel label = new JLabel(resources.getLabel(key));
 182  6
         label.setLabelFor(target);
 183  6
         return label;
 184  
     }
 185  
 
 186  
     /**
 187  
      * Various interfaces that we need to implement. We do that in an internal class
 188  
      * in order to avoid exposing publicly the methods that we implement. Only one
 189  
      * instance of this class is created for an instance of {@code IIOMetdataPanel}.
 190  
      *
 191  
      * @author Martin Desruisseaux (Geomatys)
 192  
      * @version 3.08
 193  
      *
 194  
      * @since 3.05
 195  
      * @module
 196  
      */
 197  
     private final class Controller implements ActionListener, TreeSelectionListener {
 198  
         /**
 199  
          * The component which hold every tables. This is the component that appear in the center of
 200  
          * this {@code IIOMetadataPanel}. Its layout manager must be an instance of {@link CardLayout}.
 201  
          */
 202  
         private final JComponent tables;
 203  
 
 204  
         /**
 205  
          * The selected format, or {@code null} if none.
 206  
          */
 207  
         private IIOMetadataChoice selectedFormat;
 208  
 
 209  
         /**
 210  
          * The table which is currently visible, or {@code null} if none.
 211  
          */
 212  
         private IIOMetadataTreeTable visibleTable;
 213  
 
 214  
         /**
 215  
          * Creates a new instance.
 216  
          */
 217  2
         Controller(final JComponent tables) {
 218  2
             this.tables = tables;
 219  2
         }
 220  
 
 221  
         /**
 222  
          * Resets this controller in the same state than after construction.
 223  
          */
 224  
         final void reset() {
 225  
             // Do not set visibleTable to null, because we want to copy its layout
 226  
             // when a new table will be created even if the previous table was for
 227  
             // an other image.
 228  0
             selectedFormat = null;
 229  0
             tables.removeAll();
 230  0
         }
 231  
 
 232  
         /**
 233  
          * Invoked when a new format has been selected in the combo box.
 234  
          * When a change is detected, the tree is immediately updated.
 235  
          */
 236  
         @Override
 237  
         public void actionPerformed(final ActionEvent event) {
 238  1
             final JComboBox choices = (JComboBox) event.getSource();
 239  1
             final IIOMetadataChoice oldFormat = selectedFormat;
 240  1
             final Object selected = choices.getSelectedItem();
 241  1
             if (!(selected instanceof IIOMetadataChoice)) {
 242  
                 /*
 243  
                  * This happen if the user selected the separator.
 244  
                  */
 245  0
                 choices.setSelectedItem(oldFormat);
 246  0
                 return;
 247  
             }
 248  1
             final IIOMetadataChoice newFormat = (IIOMetadataChoice) selected;
 249  1
             if (newFormat == null) {
 250  
                 /*
 251  
                  * May be null if the user selected the choice which was already selected,
 252  
                  * which have the effect of unselecting it. We want the current format to
 253  
                  * stay selected.
 254  
                  */
 255  0
                 choices.setSelectedItem(oldFormat);
 256  0
                 return;
 257  
             }
 258  1
             if (newFormat == oldFormat) {
 259  0
                 return;
 260  
             }
 261  1
             show(newFormat);
 262  1
         }
 263  
 
 264  
         /**
 265  
          * Shows the {@code TreeTable} associated with the given choice.
 266  
          * It is the caller's responsibility to ensure that the given
 267  
          * format is the one selected in the combo box.
 268  
          */
 269  
         final void show(final IIOMetadataChoice newFormat) {
 270  1
             selectedFormat = newFormat;
 271  1
             visibleTable = newFormat.show(tables, this, visibleTable);
 272  1
             showProperties(visibleTable.selectedNode);
 273  1
         }
 274  
 
 275  
         /**
 276  
          * Invoked when a node has been selected.
 277  
          */
 278  
         @Override
 279  
         public void valueChanged(final TreeSelectionEvent event) {
 280  0
             final TreePath path = event.getNewLeadSelectionPath();
 281  0
             if (path != null) {
 282  0
                 final MetadataTreeNode node = (MetadataTreeNode) path.getLastPathComponent();
 283  0
                 visibleTable.selectedNode = node;
 284  0
                 showProperties(node);
 285  
             }
 286  0
         }
 287  
     }
 288  
 
 289  
     /**
 290  
      * Fills the "properties" section in the bottom of this {@code IIOMetadataPanel}
 291  
      * using the information provided by the given node.
 292  
      *
 293  
      * @param node The selected node, for which to display the information in the bottom
 294  
      *        of this panel. Can be null.
 295  
      */
 296  
     final void showProperties(final MetadataTreeNode node) {
 297  1
         if (node == null) {
 298  1
             description.setText(null);
 299  1
             validValues.setText(null);
 300  
         } else {
 301  
             /*
 302  
              * Get the description of the given node. If no description is found for that node,
 303  
              * search for the parent until a description is found. We do that way mostly because
 304  
              * when an element contains only one attribute, some format don't provide a description
 305  
              * for that attribute since it is redundant with the element description.
 306  
              */
 307  0
             MetadataTreeNode parent = node;
 308  
             String text;
 309  0
             do text = parent.getDescription();
 310  0
             while (text == null && (parent = parent.getParent()) != null);
 311  0
             if (text == null) {
 312  0
                 text = node.getLabel();
 313  
             }
 314  0
             description.setText(text);
 315  
             /*
 316  
              * Now get the description of valid values. If there is none, we will build
 317  
              * one from the data type.
 318  
              */
 319  0
             Object restriction = node.getValueRestriction();
 320  0
             if (restriction == null) {
 321  0
                 Class<?> type = node.getValueType();
 322  0
                 if (type != null) {
 323  0
                     if (type.isArray()) {
 324  0
                         type = type.getComponentType();
 325  
                     }
 326  0
                     StringBuilder buffer = new StringBuilder(Classes.getShortName(type));
 327  0
                     final NumberRange<Integer> occurrences = node.getOccurrences();
 328  0
                     if (occurrences != null) {
 329  0
                         final String s = occurrences.toString();
 330  0
                         if (s.startsWith("[")) {
 331  0
                             buffer.append(s);
 332  
                         } else {
 333  0
                             buffer.append('[').append(s).append(']');
 334  
                         }
 335  
                     }
 336  0
                     restriction = buffer;
 337  
                 }
 338  
             }
 339  0
             validValues.setText(restriction != null ? restriction.toString() : null);
 340  
         }
 341  1
     }
 342  
 
 343  
     /**
 344  
      * Removes all metadata from this widget. After the invocation of this method,
 345  
      * this panel will be in the same state than after construction.
 346  
      */
 347  
     public void clear() {
 348  0
         formatChoices.removeAllElements();
 349  0
         controller   .reset();
 350  0
     }
 351  
 
 352  
     /**
 353  
      * Clears the previous metadata content and adds the values of the given <em>stream</em> and
 354  
      * <em>image</em> metadata. Invoking this method is equivalent to invoking {@link #clear()}
 355  
      * followed by {@link #addMetadata(IIOMetadata, IIOMetadata[]) addMetadata(...)}, except that
 356  
      * the metadata initially show will be for the same format than the one currently selected,
 357  
      * if this format exists in the new metadata.
 358  
      *
 359  
      * @param stream The stream metadata, or {@code null} if none.
 360  
      * @param image  The image metadata for each image in a file.
 361  
      *
 362  
      * @since 3.09
 363  
      */
 364  
     public void setMetadata(final IIOMetadata stream, final IIOMetadata... image) {
 365  0
         final Object selected = formatChoices.getSelectedItem();
 366  0
         clear();
 367  0
         addMetadata(stream, image);
 368  0
         final int index = formatChoices.getIndexOf(selected);
 369  0
         if (index > 0) { // Intentionnaly skip the first choice, since it is already selected.
 370  0
             final Object newFormat = formatChoices.getElementAt(index);
 371  0
             if (newFormat instanceof IIOMetadataChoice) {
 372  0
                 formatChoices.setSelectedItem(newFormat);
 373  0
                 controller.show((IIOMetadataChoice) newFormat);
 374  
             }
 375  
         }
 376  0
     }
 377  
 
 378  
     /**
 379  
      * Adds to this panel the values of the given <em>stream</em> and <em>image</em> metadata.
 380  
      * Note that this method is typically invoked alone; there is no need to invoke
 381  
      * {@link #addMetadataFormat addMetadataFormat} prior this method.
 382  
      *
 383  
      * @param stream The stream metadata, or {@code null} if none.
 384  
      * @param image  The image metadata for each image in a file.
 385  
      */
 386  
     public void addMetadata(final IIOMetadata stream, final IIOMetadata... image) {
 387  0
         final Map<IIOMetadata,Integer> imageIndex = new IdentityHashMap<IIOMetadata,Integer>();
 388  0
         final Map<String, List<IIOMetadata>> metadataForNames =
 389  
                 new LinkedHashMap<String, List<IIOMetadata>>();
 390  0
         addFormatNames(stream, metadataForNames);
 391  0
         imageIndex.put(stream, -1);
 392  0
         if (image != null) {
 393  0
             for (int i=0; i<image.length; i++) {
 394  0
                 final IIOMetadata metadata = image[i];
 395  0
                 addFormatNames(metadata, metadataForNames);
 396  0
                 imageIndex.put(metadata, i);
 397  
             }
 398  
         }
 399  
         /*
 400  
          * At this point, we grouped every metadata by format name and we remember the
 401  
          * image index for each metadata. Now process to the addition to the combo box.
 402  
          * If the format is already present in the combo box, insert right after the
 403  
          * existing format.
 404  
          */
 405  0
         final Locale locale = getLocale();
 406  0
         for (final Map.Entry<String, List<IIOMetadata>> entry : metadataForNames.entrySet()) {
 407  0
             int insertAt = -1; // Where to insert, or -1 for adding to the end of the list.
 408  0
             final String formatName = entry.getKey();
 409  0
             for (int i=formatChoices.getSize(); --i>=0;) {
 410  0
                 final Object existing = formatChoices.getElementAt(i);
 411  0
                 if (existing instanceof IIOMetadataChoice) {
 412  0
                     if (formatName.equals(((IIOMetadataChoice) existing).getFormatName())) {
 413  0
                         insertAt = i;
 414  0
                         break;
 415  
                     }
 416  
                 }
 417  0
             }
 418  0
             if (insertAt < 0 && formatChoices.getSize() != 0) {
 419  0
                 formatChoices.addElement(ComboBoxRenderer.SEPARATOR);
 420  
             }
 421  0
             final List<String> names = imageNames;
 422  0
             final int namesCount = (names != null) ? names.size() : 0;
 423  0
             for (final IIOMetadata metadata : entry.getValue()) {
 424  0
                 final int index = imageIndex.get(metadata); // Should never be null.
 425  0
                 final String name = (index >= 0 && index < namesCount) ? names.get(index) : null;
 426  0
                 final IIOMetadataChoice choice = new IIOMetadataChoice(locale, formatName, metadata, index, name);
 427  0
                 if (insertAt >= 0) {
 428  0
                     formatChoices.insertElementAt(choice, ++insertAt);
 429  
                 } else {
 430  0
                     formatChoices.addElement(choice);
 431  
                 }
 432  0
             }
 433  0
         }
 434  0
     }
 435  
 
 436  
     /**
 437  
      * Adds the metadata format names to the keys of the given map, and the metadata
 438  
      * to the values. The given metadata can be {@code null} (as authorized by the
 439  
      * {@link #addMetadata} method contract), in which case it is ignored.
 440  
      */
 441  
     private static void addFormatNames(final IIOMetadata metadata,
 442  
             final Map<String, List<IIOMetadata>> metadataForNames)
 443  
     {
 444  0
         if (metadata != null) {
 445  0
             final String[] formatNames = metadata.getMetadataFormatNames();
 446  0
             moveAtEnd(formatNames, IIOMetadataFormatImpl.standardMetadataFormatName);
 447  0
             moveAtEnd(formatNames, metadata.getNativeMetadataFormatName());
 448  0
             for (final String formatName : formatNames) {
 449  0
                 List<IIOMetadata> list = metadataForNames.get(formatName);
 450  0
                 if (list == null) {
 451  0
                     list = new ArrayList<IIOMetadata>();
 452  0
                     metadataForNames.put(formatName, list);
 453  
                 }
 454  0
                 list.add(metadata);
 455  
             }
 456  
         }
 457  0
     }
 458  
 
 459  
     /**
 460  
      * If the {@code toMove} name is found in the given array, move it at the end of the array.
 461  
      */
 462  
     private static void moveAtEnd(final String[] names, final String toMove) {
 463  0
         if (toMove != null) {
 464  0
             for (int i=0; i<names.length; i++) {
 465  0
                 final String name = names[i];
 466  0
                 if (toMove.equals(name)) {
 467  0
                     System.arraycopy(names, i+1, names, i, names.length - (i+1));
 468  0
                     names[names.length - 1] = name;
 469  0
                     break;
 470  
                 }
 471  
             }
 472  
         }
 473  0
     }
 474  
 
 475  
     /**
 476  
      * Adds to this panel the description of the given <em>stream</em> and <em>image</em>
 477  
      * metadata formats. The descriptions contain no metadata value, only the name of the
 478  
      * nodes together with a few additional information (type, valid values, <i>etc.</i>).
 479  
      *
 480  
      * @param stream The stream metadata format, or {@code null} if none.
 481  
      * @param image  The image metadata format, or {@code null} if none.
 482  
      */
 483  
     public void addMetadataFormat(final IIOMetadataFormat stream, final IIOMetadataFormat image) {
 484  2
         final Locale locale = getLocale();
 485  2
         if (stream != null) {
 486  1
             formatChoices.addElement(new IIOMetadataChoice(locale, stream, true));
 487  
         }
 488  2
         if (image != null) {
 489  2
             formatChoices.addElement(new IIOMetadataChoice(locale, image, false));
 490  
         }
 491  2
     }
 492  
 
 493  
     /**
 494  
      * Adds to this panel the description of
 495  
      * {@value org.geotoolkit.image.io.metadata.SpatialMetadataFormat#FORMAT_NAME} and
 496  
      * {@value javax.imageio.metadata.IIOMetadataFormatImpl#standardMetadataFormatName} formats.
 497  
      * The descriptions contain no metadata value, only the name of the nodes together with a few
 498  
      * additional information (type, valid values, <i>etc.</i>).
 499  
      */
 500  
     public void addDefaultMetadataFormats() {
 501  1
         addMetadataFormat(SpatialMetadataFormat.getStreamInstance(null),
 502  
                           SpatialMetadataFormat.getImageInstance (null));
 503  1
         addMetadataFormat(null, IIOMetadataFormatImpl.getStandardFormatInstance());
 504  1
     }
 505  
 }