/* * Geotoolkit.org - An Open Source Java GIS Toolkit * http://www.geotoolkit.org * * (C) 2007-2012, Open Source Geospatial Foundation (OSGeo) * (C) 2007-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.coverage; import java.util.Set; import java.util.Date; import java.util.List; import java.util.Locale; import java.util.Arrays; import java.util.SortedSet; import java.util.Collections; import java.util.Objects; import java.util.concurrent.ExecutionException; import java.io.File; import java.awt.Dimension; import java.awt.BorderLayout; import java.awt.GridLayout; import java.awt.CardLayout; import java.awt.EventQueue; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import javax.swing.BorderFactory; import javax.swing.Box; import javax.swing.JButton; import javax.swing.JComponent; import javax.swing.JList; import javax.swing.JPanel; import javax.swing.JScrollPane; import javax.swing.JSplitPane; import javax.swing.JTable; import javax.swing.SwingWorker; import javax.swing.ListSelectionModel; import javax.swing.event.ListSelectionEvent; import javax.swing.event.ListSelectionListener; import javax.swing.table.TableCellRenderer; import org.jdesktop.swingx.JXTitledPanel; import org.opengis.util.FactoryException; import org.opengis.referencing.crs.CRSAuthorityFactory; import org.geotoolkit.coverage.sql.Layer; import org.geotoolkit.coverage.sql.CoverageTableModel; import org.geotoolkit.coverage.sql.CoverageEnvelope; import org.geotoolkit.coverage.sql.CoverageDatabase; import org.geotoolkit.coverage.sql.GridCoverageReference; import org.geotoolkit.coverage.sql.DatabaseVetoException; import org.geotoolkit.coverage.io.CoverageStoreException; import org.geotoolkit.gui.swing.image.ImageFileProperties; import org.geotoolkit.gui.swing.image.ImageFileChooser; import org.geotoolkit.gui.swing.IconFactory; import org.geotoolkit.internal.swing.ToolBar; import org.geotoolkit.internal.swing.ExceptionMonitor; import org.geotoolkit.resources.Vocabulary; import org.geotoolkit.factory.AuthorityFactoryFinder; import org.geotoolkit.internal.swing.SwingUtilities; import org.geotoolkit.resources.Widgets; import org.jdesktop.swingx.JXBusyLabel; /** * A list displaying the {@linkplain Layer#getCoverageReferences(CoverageEnvelope) set of * coverages} available in a given layer. This widget displays also the properties of the * selected file on the right side. * * @author Martin Desruisseaux (Geomatys) * @version 3.17 * * @see CoverageTableModel * @see <a href="{@docRoot}/../modules/display/geotk-wizards-swing/AddCoverages.html">Adding layers and images to the Coverage-SQL database</a> * * @since 3.11 (derived from Seagis) * @module */ @SuppressWarnings("serial") public class CoverageList extends JComponent { /** * Action commands. */ private static final String ADD="ADD", REMOVE="REMOVE", REFRESH="REFRESH"; /** * Layout parameters for the components put in {@link #selectionPanel}. */ static final String TABLE="TABLE", FILES="FILES", VARIABLES="VARIABLES", CONTROLLER="CONTROLLER", BUZY="BUZY"; /** * The name of the panel currently selected in {@link #selectionPanel}. */ private String selectionPanelName; /** * The panel where to select a coverage, either from the table of coverages * or from a file chooser. This panel uses a {@link CardLayout}. */ private final JPanel selectionPanel; /** * The file chooser, created only when the user click the "add" button for the first time. */ private ImageFileChooser fileChooser; /** * The variable chooser, created only when the user select a file for the first time. * * @since 3.15 */ private JList<String> variableChooser; /** * The table which list all coverages. */ private final JTable table; /** * The list of coverages for the selected layer. */ private final CoverageTableModel coverages; /** * The layer shown by this widget. */ private Layer layer; /** * The spatio-temporal envelope to query, or {@code null} for the full coverage. */ private CoverageEnvelope envelope; /** * The properties of the selected image. */ final ImageFileProperties properties; /** * The label to display when the widget is busy loading the properties of an image. * This happen before the {@link #addController} is initialized with the new values. */ private JXBusyLabel busyLabel; /** * The panel for adding new files. Will be created only when first needed. */ private NewGridCoverageDetails addController; /** * The toolbar, to be enabled or disabled depending on the view currently active * in {@link #selectionPanel}. */ private final ToolBar toolbar; /** * The button for removing entries. To be enabled only when at least * one entry is selected. */ private final JButton removeButton; /** * Listeners used for various actions. */ private final Listeners listeners; /** * Creates a new list with a default, initially empty, {@code CoverageTableModel}. */ public CoverageList() { this(new CoverageTableModel((Locale) null)); } /** * Creates a list for the specified collection of coverages. * * @param coverages The table model which contain the coverage entries to list. */ public CoverageList(final CoverageTableModel coverages) { setLayout(new BorderLayout()); // Required for the toolbar. this.coverages = coverages; final Locale locale = getLocale(); final Vocabulary resources = Vocabulary.getResources(locale); listeners = new Listeners(); final JTable table = new JTable(coverages); final TableCellRenderer renderer = new CoverageTableModel.CellRenderer(); table.setDefaultRenderer(String.class, renderer); table.setDefaultRenderer(Date.class, renderer); table.getSelectionModel().addListSelectionListener(listeners); this.table = table; final Dimension minimumSize = new Dimension(120, 100); selectionPanel = new JPanel(new CardLayout()); selectionPanel.add(new JScrollPane(table), TABLE); selectionPanel.setMinimumSize(minimumSize); selectionPanelName = TABLE; properties = new CoverageFileProperties(); properties.setMinimumSize(minimumSize); properties.setPreferredSize(new Dimension(440, 400)); final JSplitPane pane = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, selectionPanel, properties); pane.setOneTouchExpandable(true); pane.setContinuousLayout(true); pane.setBorder(BorderFactory.createEmptyBorder(9, 3, 9, 9)); /* * The buttons bar. */ toolbar = new ToolBar(resources.getString(Vocabulary.Keys.Edit), ToolBar.VERTICAL); toolbar.setRollover(true); button(resources, Vocabulary.Keys.Add, "toolbarButtonGraphics/table/RowInsertBefore24.gif", ADD); removeButton = button(resources, Vocabulary.Keys.Remove, "toolbarButtonGraphics/table/RowDelete24.gif", REMOVE); removeButton.setEnabled(false); button(resources, Vocabulary.Keys.Refresh, "toolbarButtonGraphics/general/Refresh24.gif", REFRESH); /* * Put the components in this panel. */ add(BorderLayout.CENTER, pane); add(toolbar, BorderLayout.WEST); } /** * Creates a new button and adds it to the toolbar. */ private JButton button(final Vocabulary resources, final short key, final String image, final String action) { final String text = resources.getString(key); final JButton button = IconFactory.DEFAULT.getButton(image, text, text); button.setActionCommand(action); button.addActionListener(listeners); toolbar.add(button); return button; } /** * Implement all listeners used by the {@link LayerList} class. */ private final class Listeners implements ListSelectionListener, ActionListener { /** * The last selected entry. Used in order to detect if the selection changed, * in order to avoid unnecessary fetching of image properties. */ private GridCoverageReference last; /** * Invoked when a coverage has been selected. This method enable the "remove" * button if at least one entry is selected, then read the properties of the * selected image in a background thread. */ @Override public void valueChanged(final ListSelectionEvent event) { if (event.getValueIsAdjusting()) { return; } final ListSelectionModel model = (ListSelectionModel) event.getSource(); final boolean isEmpty = model.isSelectionEmpty(); removeButton.setEnabled(!isEmpty); GridCoverageReference reference = null; if (!isEmpty) { final int coverageIndex = model.getAnchorSelectionIndex(); reference = coverages.getCoverageReferenceAt(coverageIndex); if (reference == last) { return; } } last = reference; setImageProperties(reference); } /** * Invoked when one of the buttons ("Remove", "Add", etc.) has been pressed. * This method delegates to the appropriate method in the enclosing class. */ @Override public void actionPerformed(final ActionEvent event) { final String action = event.getActionCommand(); switch (action) { case REFRESH: { refresh(); break; } case REMOVE: { removeCoverage(); break; } case ADD: { showFileChooser(); break; } case ImageFileChooser.CANCEL_SELECTION: { setSelectionPanel(TABLE); break; } case ImageFileChooser.APPROVE_SELECTION: { // Must check if the file selection panel is visible, because pressing the 'Enter' // key in the format JComboBox of the NewGridCoverageDetails widget seems to also // fire the event associated with "Ok" button of the JFileChooser. if (FILES.equals(selectionPanelName)) { addNewCoverage(); } break; } } } } /** * Returns the layer for which this widget is listing the coverages. * If the layer is unknown, then this method returns {@code null}. * * @return The current layer, or {@code null} if unknown. */ public Layer getLayer() { return layer; } /** * Sets the content of this widget to the list of coverages in the given layer. * This method will fetch the list of coverage entries in a background thread. * * @param layer The layer for which to get the coverage entries, or {@code null} if none. */ public void setLayer(final Layer layer) { if (!Objects.equals(layer, this.layer)) { setData(layer, envelope); } } /** * Returns the envelope of the listed coverage entries, or {@code null} if there * is no restriction. If non-null, then this widget list only the coverage entries * which intersect the returned envelope. * * @return The envelope of the listed coverage entries, or {@code null} if unbounded. */ public CoverageEnvelope getEnvelope() { return (envelope != null) ? envelope.clone() : null; } /** * Sets the envelope of coverage entries to list. IF the given envelope is non-null, then * this widget will list only the coverage entries which intersect the given envelope. * <p> * If a {@linkplain #setLayer(Layer) layer has been set}, then this method will refresh * the list of coverage entries in a background thread. * * @param envelope The envelope of the coverage entries to list, or {@code null} if unbounded. */ public void setEnvelope(CoverageEnvelope envelope) { if (!Objects.equals(envelope, this.envelope)) { setData(layer, envelope); } } /** * Sets the content of this widget to the list of coverages in the given layer which * intersect the given envelope. This method combines {@link #setLayer(Layer)} and * {@link #setEnvelope(CoverageEnvelope)} in a single method call. * <p> * This method will fetch the list of coverage references in a background thread. * * @param layer The layer for which to get the coverage entries, or {@code null} if none. * @param envelope The envelope of the coverage entries to list, or {@code null} if unbounded. */ final void setData(final Layer layer, final CoverageEnvelope envelope) { final Layer oldLayer = this.layer; final CoverageEnvelope oldEnvelope = this.envelope; this.layer = layer; this.envelope = envelope; if (layer == null) { coverages.setCoverageReferences(Collections.<GridCoverageReference>emptyList()); } else { final SwingWorker<Set<GridCoverageReference>,Object> worker = new SwingWorker<Set<GridCoverageReference>,Object>() { /** * Invoked in a background thread for fetching the list of layers. */ @Override protected Set<GridCoverageReference> doInBackground() throws CoverageStoreException { return layer.getCoverageReferences(envelope); } /** * Invoked in the Swing thread for settings the table content. */ @Override protected void done() { Exception cause; try { coverages.setCoverageReferences(get()); return; } catch (InterruptedException ex) { cause = ex; } catch (ExecutionException ex) { final Throwable c = ex.getCause(); cause = (c instanceof Exception) ? (Exception) c : ex; } exceptionOccured(cause); } }; worker.execute(); } firePropertyChange("layer", layer, oldLayer); firePropertyChange("envelope", envelope, oldEnvelope); } /** * Reloads the list of coverages in the {@linkplain #getLayer() current layer} which * intersect the {@linkplain #getEnvelope() current envelope}. * * @since 3.12 */ public void refresh() { setData(layer, envelope); } /** * Shows the properties of the given coverage reference. The properties are shown * in the right side of the split pane. * * @param reference The reference to a grid coverage, or {@code null} if none. */ private void setImageProperties(final GridCoverageReference reference) { properties.setImageLater(reference); } /** * Sets the selection panel (the component on the left side of the split panel) * to the table or to the file chooser. The component to be shown is controlled * by a {@link CardLayout}. * * @param name The name of the components to show, either {@link #TABLE}, {@link #FILES} * or {@link #CONTROLLER}. */ final void setSelectionPanel(final String name) { ((CardLayout) selectionPanel.getLayout()).show(selectionPanel, name); selectionPanelName = name; toolbar.setButtonsEnabled(TABLE.equals(name)); switch (name) { case TABLE: { final ListSelectionModel model = table.getSelectionModel(); setImageProperties(model.isSelectionEmpty() ? null : coverages.getCoverageReferenceAt(model.getAnchorSelectionIndex())); break; } case FILES: { properties.setImageLater(fileChooser.getSelectedFile()); break; } } } /** * Ensures that the file chooser is ready to be shown. If the user confirm * his selection, then {@link #selectVariables()} will be invoked later. */ private void showFileChooser() { final Layer layer = getLayer(); if (layer != null) { if (fileChooser == null) { final SortedSet<String> formats; final SortedSet<File> directories; try { formats = layer.getImageFormats(); directories = layer.getImageDirectories(); } catch (CoverageStoreException e) { ExceptionMonitor.show(this, e); return; } fileChooser = new ImageFileChooser(formats.isEmpty() ? "png" : formats.first(), true); fileChooser.setDialogType(ImageFileChooser.OPEN_DIALOG); fileChooser.setPropertiesPane(properties); fileChooser.addActionListener(listeners); for (final File directory : directories) { if (directory.isDirectory()) { fileChooser.setCurrentDirectory(directory); break; } } selectionPanel.add(new JXTitledPanel(Widgets.getResources(getLocale()) .getString(Widgets.Keys.SelectFile), fileChooser), FILES); /* * Creates the busy panel. */ busyLabel = new JXBusyLabel(new Dimension(60, 60)); busyLabel.setHorizontalAlignment(JXBusyLabel.CENTER); selectionPanel.add(busyLabel, BUZY); } setSelectionPanel(FILES); } } /** * Potentially invoked after {@link #addNewCoverage()} started its work. This method shows * a list of available images in the file. * * @since 3.15 */ final void showVariableChooser(final String[] images, final boolean multiSelectionAllowed) { if (variableChooser == null) { variableChooser = new JList<>(); final Vocabulary resources = Vocabulary.getResources(getLocale()); final JButton ok = new JButton(resources.getString(Vocabulary.Keys.Ok)); final JButton cancel = new JButton(resources.getString(Vocabulary.Keys.Cancel)); final JPanel buttons = new JPanel(new GridLayout(1, 2, 6, 0)); ok .addActionListener(addController); cancel.addActionListener(addController); ok .setActionCommand(NewGridCoverageDetails.SELECT_VARIABLES); cancel.setActionCommand(NewGridCoverageDetails.CANCEL); buttons.add(ok); buttons.add(cancel); buttons.setOpaque(false); buttons.setBorder(BorderFactory.createEmptyBorder(6, 0, 6, 0)); final Box buttonBar = Box.createHorizontalBox(); buttonBar.add(Box.createHorizontalGlue()); buttonBar.add(buttons); buttonBar.add(Box.createHorizontalGlue()); buttonBar.setOpaque(false); final JPanel panel = new JPanel(new BorderLayout()); panel.add(variableChooser, BorderLayout.CENTER); panel.add(buttonBar, BorderLayout.AFTER_LAST_LINE); selectionPanel.add(new JXTitledPanel(Widgets.getResources(getLocale()) .getString(Widgets.Keys.SelectVariables), panel), VARIABLES); } variableChooser.setListData(images); variableChooser.setSelectionMode(multiSelectionAllowed ? ListSelectionModel.MULTIPLE_INTERVAL_SELECTION : ListSelectionModel.SINGLE_SELECTION); setSelectionPanel(VARIABLES); } /** * Invoked after {@link #showVariableChooser(String[], boolean)} in order to get the * variables selected by the user. A side effect of this method is to show again the * busy panel, since this method is invoked when {@link #addNewCoverage()} is about * to continue its work. * * @since 3.15 */ final List<String> getSelectedVariables() { setSelectionPanel(BUZY); return variableChooser.getSelectedValuesList(); } /** * Invoked when the user confirmed his selection in the file chooser. If the user selected * a file, then the {@link NewGridCoverageDetails} window will be shown for each file to be * added in the database. */ private void addNewCoverage() { setSelectionPanel(BUZY); busyLabel.setBusy(true); /* * If the user confirmed his selection, create the controller (if not already * done), then starts the image addition process in a background thread. */ final File[] files = fileChooser.getSelectedFiles(); if (files != null && files.length != 0) { if (addController == null) { CRSAuthorityFactory factory = null; final CoverageDatabase database = layer.getCoverageDatabase(); if (database != null) try { factory = database.getCRSAuthorityFactory(); } catch (FactoryException e) { exceptionOccured(e); } if (factory == null) { factory = AuthorityFactoryFinder.getCRSAuthorityFactory("EPSG", null); } addController = new NewGridCoverageDetails(this, factory); selectionPanel.add(addController, CONTROLLER); } /* * Runs a worker in a background thread for adding the new coverages. * We don't use the SwingWorker because NewGridCoverageDetails will * block waiting for user input from the event thread, and it seems * to prevent other SwingWorkers to work. * * Note: don't use Threads.executeWork(...) neither, since it is designed * for short-lived task (running this task could block other tasks if the * thread pool is full). */ new Thread(SwingUtilities.THREAD_GROUP, new Runnable() { @Override public void run() { try { layer.addCoverageReferences(Arrays.asList(files), addController); } catch (DatabaseVetoException e) { // User cancelled the operation or closed the frame. // Do not report the exception since it is intentional. } catch (Exception e) { exceptionOccured(e); } finally { EventQueue.invokeLater(new Runnable() { @Override public void run() { busyLabel.setBusy(false); setSelectionPanel(TABLE); refresh(); } }); } } }, "CoverageList").start(); } } /** * Invoked when the user pressed the "Remove" button. * * @todo Current implementation remove only the row from the JTable. * It does not yet update the database. */ private void removeCoverage() { coverages.remove(table.getSelectedRows()); } /** * Invoked when an exception occurred while querying the {@linkplain Layer layer}. * The default implementation reports the error in an {@link ExceptionMonitor}. * Subclasses can override this method in order to report the error in a different way. * * @param ex The exception which occurred. */ protected void exceptionOccured(final Exception ex) { ExceptionMonitor.show(this, ex); } }