/*
* Geotoolkit.org - An Open Source Java GIS Toolkit
* http://www.geotoolkit.org
*
* (C) 2009-2012, Open Source Geospatial Foundation (OSGeo)
* (C) 2009-2012, Geomatys
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation;
* version 2.1 of the License.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*/
package org.geotoolkit.gui.swing.image;
import java.util.Map;
import java.util.List;
import java.util.Locale;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.IdentityHashMap;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import javax.swing.*;
import javax.swing.tree.TreePath;
import javax.swing.event.TreeSelectionEvent;
import javax.swing.event.TreeSelectionListener;
import javax.imageio.metadata.IIOMetadata;
import javax.imageio.metadata.IIOMetadataFormat;
import javax.imageio.metadata.IIOMetadataFormatImpl;
import org.jdesktop.swingx.JXTreeTable;
import org.geotoolkit.resources.Vocabulary;
import org.apache.sis.measure.NumberRange;
import org.apache.sis.util.Classes;
import org.geotoolkit.image.io.metadata.MetadataTreeNode;
import org.geotoolkit.image.io.metadata.MetadataTreeTable;
import org.geotoolkit.image.io.metadata.SpatialMetadataFormat;
import org.geotoolkit.internal.swing.ComboBoxRenderer;
import static org.geotoolkit.image.io.metadata.SpatialMetadataFormat.GEOTK_FORMAT_NAME;
/**
* A panel showing the content of an {@link IIOMetadata} instance. This panel contains three parts:
* <p>
* <ul>
* <li>At the top, a field allowing to select which metadata to display:
* <ul>
* <li>The metadata format, typically
* {@value org.geotoolkit.image.io.metadata.SpatialMetadataFormat#GEOTK_FORMAT_NAME} or
* {@value javax.imageio.metadata.IIOMetadataFormatImpl#standardMetadataFormatName}.
* </li>
* <li>The metadata part to display: <cite>stream</cite> metadata or one of the
* <cite>image</cite> metadata.</li>
* </ul>
* </li>
* <li>At the center, the metadata as a {@linkplain JXTreeTable tree table}.
* The columns are documented in {@link MetadataTreeTable} javadoc.</li>
* <li>At the bottom, a description of the currently selected metadata node.</li>
* </ul>
* <p>
* Most columns are hiden by default. The initial view shows only (<var>name</var>, <var>value</var>) pairs in
* the {@link IIOMetadata} case, or (<var>name</var>, <var>type</var>) pairs in the {@link IIOMetadataFormat}
* case. Users can make additional columns visible by clicking on the icon in the upper-right corner.
* <p>
* This class can be used in two ways (choose only one):
* <p>
* <ul>
* <li>For displaying the structure of {@link IIOMetadataFormat} instances without data,
* invoke {@link #addMetadataFormat addMetadataFormat(...)}.</li>
*
* <li>For displaying the actual content of {@link IIOMetadata} instances, invoke
* {@link #addMetadata addMetadata(...)}.</li>
* </ul>
*
* <table cellspacing="24" cellpadding="12" align="center"><tr valign="top">
* <td width="500" bgcolor="lightblue">
* {@section Demo}
* To try this component in your browser, see the
* <a href="http://www.geotoolkit.org/demos/geotk-simples/applet/IIOMetadataPanel.html">demonstration applet</a>.
* </td></tr></table>
*
* @author Martin Desruisseaux (Geomatys)
* @version 3.12
*
* @see MetadataTreeTable
*
* @since 3.05
* @module
*/
@SuppressWarnings("serial")
public class IIOMetadataPanel extends JComponent {
/**
* The choices of metadata format. Typical choices are
* {@value org.geotoolkit.image.io.metadata.SpatialMetadataFormat#GEOTK_FORMAT_NAME} and
* {@value javax.imageio.metadata.IIOMetadataFormatImpl#standardMetadataFormatName}.
*/
private final DefaultComboBoxModel<Object> formatChoices;
/**
* The properties of the currently selected metadata node.
*/
private final JLabel description, validValues;
/**
* The unique instance of the set of listeners which is associated to this panel.
*/
private final Controller controller;
/**
* The name of images for each index. This is an undocumented feature for now.
*/
transient List<String> imageNames;
/**
* Creates a panel with no initial metadata. One of the {@code addXXXMetadata} or
* {@code addXXXMetadataFormat} methods should be invoked in order to display a content.
*/
public IIOMetadataPanel() {
setLayout(new BorderLayout());
// If the preferred width is modified, consider updating the
// preferred column width in IIOMetadataTreeTable constructor.
setPreferredSize(new Dimension(500, 400));
final JComponent tables = new JPanel(new CardLayout());
add(tables, BorderLayout.CENTER);
final Vocabulary resources = Vocabulary.getResources(getLocale());
/*
* Add the control button on top of the metadata table.
*/
formatChoices = new DefaultComboBoxModel<>();
final JComboBox<Object> formats = new JComboBox<>(formatChoices);
ComboBoxRenderer.install(formats);
formats.setName("Formats");
if (true) {
final JPanel controls = new JPanel(new GridBagLayout());
final GridBagConstraints c = new GridBagConstraints();
final Insets ci = c.insets;
ci.left = 12;
c.gridy=0; ci.top=6; ci.bottom=0;
c.gridx=0; c.weightx=0; c.anchor=GridBagConstraints.WEST;
controls.add(label(resources, Vocabulary.Keys.Format, formats), c);
c.insets.right = 12; c.insets.left = 0;
c.gridx=1; c.weightx=1; c.fill=GridBagConstraints.BOTH;
controls.add(formats, c);
add(controls, BorderLayout.NORTH);
controls.setOpaque(false);
}
/*
* Add the section for metadata properties.
*/
if (true) {
final JPanel properties = new JPanel(new GridBagLayout());
final GridBagConstraints c = new GridBagConstraints();
final Insets ci = c.insets;
c.gridx=1; c.weightx=1; c.anchor=GridBagConstraints.WEST;
c.gridy=0; ci.top=3; ci.bottom=0; properties.add(description = new JLabel(), c);
c.gridy++; ci.top=0; ci.bottom=3; properties.add(validValues = new JLabel(), c);
ci.left = 6;
c.gridx=0; c.weightx=0; ci.right=9;
c.gridy=0; ci.top=3; ci.bottom=0; properties.add(label(resources, Vocabulary.Keys.Description, description), c);
c.gridy++; ci.top=0; ci.bottom=3; properties.add(label(resources, Vocabulary.Keys.ValidValues, validValues), c);
add(properties, BorderLayout.SOUTH);
properties.setOpaque(false);
}
/*
* Plug the listeners.
*/
controller = new Controller(tables);
formats.addActionListener(controller);
}
/**
* Creates a new label for the given target component.
*/
private static JLabel label(final Vocabulary resources, final short key, final JComponent target) {
final JLabel label = new JLabel(resources.getLabel(key));
label.setLabelFor(target);
return label;
}
/**
* Various interfaces that we need to implement. We do that in an internal class
* in order to avoid exposing publicly the methods that we implement. Only one
* instance of this class is created for an instance of {@code IIOMetdataPanel}.
*
* @author Martin Desruisseaux (Geomatys)
* @version 3.08
*
* @since 3.05
* @module
*/
private final class Controller implements ActionListener, TreeSelectionListener {
/**
* The component which hold every tables. This is the component that appear in the center of
* this {@code IIOMetadataPanel}. Its layout manager must be an instance of {@link CardLayout}.
*/
private final JComponent tables;
/**
* The selected format, or {@code null} if none.
*/
private IIOMetadataChoice selectedFormat;
/**
* The table which is currently visible, or {@code null} if none.
*/
private IIOMetadataTreeTable visibleTable;
/**
* Creates a new instance.
*/
Controller(final JComponent tables) {
this.tables = tables;
}
/**
* Resets this controller in the same state than after construction.
*/
final void reset() {
// Do not set visibleTable to null, because we want to copy its layout
// when a new table will be created even if the previous table was for
// an other image.
selectedFormat = null;
tables.removeAll();
}
/**
* Invoked when a new format has been selected in the combo box.
* When a change is detected, the tree is immediately updated.
*/
@Override
public void actionPerformed(final ActionEvent event) {
final JComboBox<?> choices = (JComboBox<?>) event.getSource();
final IIOMetadataChoice oldFormat = selectedFormat;
final Object selected = choices.getSelectedItem();
if (!(selected instanceof IIOMetadataChoice)) {
/*
* This happen if the user selected the separator.
*/
choices.setSelectedItem(oldFormat);
return;
}
final IIOMetadataChoice newFormat = (IIOMetadataChoice) selected;
if (newFormat == null) {
/*
* May be null if the user selected the choice which was already selected,
* which have the effect of unselecting it. We want the current format to
* stay selected.
*/
choices.setSelectedItem(oldFormat);
return;
}
if (newFormat == oldFormat) {
return;
}
show(newFormat);
}
/**
* Shows the {@code TreeTable} associated with the given choice.
* It is the caller's responsibility to ensure that the given
* format is the one selected in the combo box.
*/
final void show(final IIOMetadataChoice newFormat) {
selectedFormat = newFormat;
visibleTable = newFormat.show(tables, this, visibleTable);
showProperties(visibleTable.selectedNode);
}
/**
* Invoked when a node has been selected.
*/
@Override
public void valueChanged(final TreeSelectionEvent event) {
final TreePath path = event.getNewLeadSelectionPath();
if (path != null) {
final MetadataTreeNode node = (MetadataTreeNode) path.getLastPathComponent();
visibleTable.selectedNode = node;
showProperties(node);
}
}
}
/**
* Fills the "properties" section in the bottom of this {@code IIOMetadataPanel}
* using the information provided by the given node.
*
* @param node The selected node, for which to display the information in the bottom
* of this panel. Can be null.
*/
final void showProperties(final MetadataTreeNode node) {
if (node == null) {
description.setText(null);
validValues.setText(null);
} else {
/*
* Get the description of the given node. If no description is found for that node,
* search for the parent until a description is found. We do that way mostly because
* when an element contains only one attribute, some format don't provide a description
* for that attribute since it is redundant with the element description.
*/
MetadataTreeNode parent = node;
String text;
do text = parent.getDescription();
while (text == null && (parent = parent.getParent()) != null);
if (text == null) {
text = node.getLabel();
}
description.setText(text);
/*
* Now get the description of valid values. If there is none, we will build
* one from the data type.
*/
Object restriction = node.getValueRestriction();
if (restriction == null) {
Class<?> type = node.getValueType();
if (type != null) {
if (type.isArray()) {
type = type.getComponentType();
}
StringBuilder buffer = new StringBuilder(Classes.getShortName(type));
final NumberRange<Integer> occurrences = node.getOccurrences();
if (occurrences != null) {
final String s = occurrences.toString();
if (s.startsWith("[")) {
buffer.append(s);
} else {
buffer.append('[').append(s).append(']');
}
}
restriction = buffer;
}
}
validValues.setText(restriction != null ? restriction.toString() : null);
}
}
/**
* Removes all metadata from this widget. After the invocation of this method,
* this panel will be in the same state than after construction.
*/
public void clear() {
formatChoices.removeAllElements();
controller .reset();
}
/**
* Clears the previous metadata content and adds the values of the given <em>stream</em> and
* <em>image</em> metadata. Invoking this method is equivalent to invoking {@link #clear()}
* followed by {@link #addMetadata(IIOMetadata, IIOMetadata[]) addMetadata(...)}, except that
* the metadata initially show will be for the same format than the one currently selected,
* if this format exists in the new metadata.
*
* @param stream The stream metadata, or {@code null} if none.
* @param image The image metadata for each image in a file.
*
* @since 3.09
*/
public void setMetadata(final IIOMetadata stream, final IIOMetadata... image) {
final Object selected = formatChoices.getSelectedItem();
clear();
addMetadata(stream, image);
final int index = formatChoices.getIndexOf(selected);
if (index > 0) { // Intentionnaly skip the first choice, since it is already selected.
final Object newFormat = formatChoices.getElementAt(index);
if (newFormat instanceof IIOMetadataChoice) {
formatChoices.setSelectedItem(newFormat);
controller.show((IIOMetadataChoice) newFormat);
}
}
}
/**
* Adds to this panel the values of the given <em>stream</em> and <em>image</em> metadata.
* Note that this method is typically invoked alone; there is no need to invoke
* {@link #addMetadataFormat addMetadataFormat} prior this method.
*
* @param stream The stream metadata, or {@code null} if none.
* @param image The image metadata for each image in a file.
*/
public void addMetadata(final IIOMetadata stream, final IIOMetadata... image) {
final Map<IIOMetadata,Integer> imageIndex = new IdentityHashMap<>();
final Map<String, List<IIOMetadata>> metadataForNames = new LinkedHashMap<>();
addFormatNames(stream, metadataForNames);
imageIndex.put(stream, -1);
if (image != null) {
for (int i=0; i<image.length; i++) {
final IIOMetadata metadata = image[i];
addFormatNames(metadata, metadataForNames);
imageIndex.put(metadata, i);
}
}
/*
* At this point, we grouped every metadata by format name and we remember the
* image index for each metadata. Now process to the addition to the combo box.
* If the format is already present in the combo box, insert right after the
* existing format.
*/
final Locale locale = getLocale();
for (final Map.Entry<String, List<IIOMetadata>> entry : metadataForNames.entrySet()) {
int insertAt = -1; // Where to insert, or -1 for adding to the end of the list.
final String formatName = entry.getKey();
for (int i=formatChoices.getSize(); --i>=0;) {
final Object existing = formatChoices.getElementAt(i);
if (existing instanceof IIOMetadataChoice) {
if (formatName.equals(((IIOMetadataChoice) existing).getFormatName())) {
insertAt = i;
break;
}
}
}
if (insertAt < 0 && formatChoices.getSize() != 0) {
formatChoices.addElement(ComboBoxRenderer.SEPARATOR);
}
final List<String> names = imageNames;
final int namesCount = (names != null) ? names.size() : 0;
for (final IIOMetadata metadata : entry.getValue()) {
final int index = imageIndex.get(metadata); // Should never be null.
final String name = (index >= 0 && index < namesCount) ? names.get(index) : null;
final IIOMetadataChoice choice = new IIOMetadataChoice(locale, formatName, metadata, index, name);
if (insertAt >= 0) {
formatChoices.insertElementAt(choice, ++insertAt);
} else {
formatChoices.addElement(choice);
}
}
}
}
/**
* Adds the metadata format names to the keys of the given map, and the metadata
* to the values. The given metadata can be {@code null} (as authorized by the
* {@link #addMetadata} method contract), in which case it is ignored.
*/
private static void addFormatNames(final IIOMetadata metadata,
final Map<String, List<IIOMetadata>> metadataForNames)
{
if (metadata != null) {
final String[] formatNames = metadata.getMetadataFormatNames();
moveAtEnd(formatNames, IIOMetadataFormatImpl.standardMetadataFormatName);
moveAtEnd(formatNames, metadata.getNativeMetadataFormatName());
for (final String formatName : formatNames) {
List<IIOMetadata> list = metadataForNames.get(formatName);
if (list == null) {
list = new ArrayList<>();
metadataForNames.put(formatName, list);
}
list.add(metadata);
}
}
}
/**
* If the {@code toMove} name is found in the given array, move it at the end of the array.
*/
private static void moveAtEnd(final String[] names, final String toMove) {
if (toMove != null) {
for (int i=0; i<names.length; i++) {
final String name = names[i];
if (toMove.equals(name)) {
System.arraycopy(names, i+1, names, i, names.length - (i+1));
names[names.length - 1] = name;
break;
}
}
}
}
/**
* Adds to this panel the description of the given <em>stream</em> and <em>image</em>
* metadata formats. The descriptions contain no metadata value, only the name of the
* nodes together with a few additional information (type, valid values, <i>etc.</i>).
*
* @param stream The stream metadata format, or {@code null} if none.
* @param image The image metadata format, or {@code null} if none.
*/
public void addMetadataFormat(final IIOMetadataFormat stream, final IIOMetadataFormat image) {
final Locale locale = getLocale();
if (stream != null) {
formatChoices.addElement(new IIOMetadataChoice(locale, stream, true));
}
if (image != null) {
formatChoices.addElement(new IIOMetadataChoice(locale, image, false));
}
}
/**
* Adds to this panel the description of
* {@value org.geotoolkit.image.io.metadata.SpatialMetadataFormat#GEOTK_FORMAT_NAME} and
* {@value javax.imageio.metadata.IIOMetadataFormatImpl#standardMetadataFormatName} formats.
* The descriptions contain no metadata value, only the name of the nodes together with a few
* additional information (type, valid values, <i>etc.</i>).
*/
public void addDefaultMetadataFormats() {
addMetadataFormat(SpatialMetadataFormat.getStreamInstance(GEOTK_FORMAT_NAME),
SpatialMetadataFormat.getImageInstance (GEOTK_FORMAT_NAME));
addMetadataFormat(null, IIOMetadataFormatImpl.getStandardFormatInstance());
}
}