/*
* Geotoolkit.org - An Open Source Java GIS Toolkit
* http://www.geotoolkit.org
*
* (C) 2010-2012, Open Source Geospatial Foundation (OSGeo)
* (C) 2010-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.awt.BorderLayout;
import java.awt.Container;
import java.awt.EventQueue;
import java.io.File;
import java.nio.file.Path;
import java.util.Collection;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
import java.text.ParseException;
import java.awt.GridLayout;
import java.awt.GridBagLayout;
import java.awt.GridBagConstraints;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import javax.swing.JPanel;
import javax.swing.JLabel;
import javax.swing.JButton;
import javax.swing.JComboBox;
import javax.swing.JCheckBox;
import javax.swing.JComponent;
import javax.swing.JTextField;
import javax.swing.JScrollPane;
import javax.swing.DefaultComboBoxModel;
import org.jdesktop.swingx.JXLabel;
import org.jdesktop.swingx.JXTaskPane;
import org.jdesktop.swingx.JXTaskPaneContainer;
import org.opengis.util.InternationalString;
import org.opengis.referencing.crs.VerticalCRS;
import org.opengis.referencing.crs.ProjectedCRS;
import org.opengis.referencing.crs.GeographicCRS;
import org.opengis.referencing.crs.CRSAuthorityFactory;
import org.geotoolkit.resources.Widgets;
import org.geotoolkit.resources.Vocabulary;
import org.apache.sis.util.ArraysExt;
import org.apache.sis.util.Disposable;
import org.apache.sis.util.logging.Logging;
import org.apache.sis.util.iso.SimpleInternationalString;
import org.geotoolkit.coverage.GridSampleDimension;
import org.geotoolkit.coverage.io.CoverageStoreException;
import org.geotoolkit.coverage.sql.CoverageDatabaseEvent;
import org.geotoolkit.coverage.sql.DatabaseVetoException;
import org.geotoolkit.coverage.sql.NewGridCoverageReference;
import org.geotoolkit.coverage.sql.CoverageDatabaseController;
import org.geotoolkit.gui.swing.WindowCreator;
import org.geotoolkit.gui.swing.referencing.AuthorityCodesComboBox;
import org.geotoolkit.internal.swing.ComponentDisposer;
import org.geotoolkit.internal.swing.SwingUtilities;
import static org.apache.sis.util.collection.Containers.isNullOrEmpty;
/**
* A form showing details about a {@link NewGridCoverageReference}.
* Users can verify and modify those information before they are written in the database.
* <p>
* An instance of this class is created by {@link CoverageList} when first needed,
* and reused for all images to be inserted from the same {@code CoverageList}.
* New images are declared by invoking {@link #coverageAdding}.
*
* @author Martin Desruisseaux (Geomatys)
* @version 3.17
*
* @since 3.12
* @module
*/
@SuppressWarnings("serial")
final class NewGridCoverageDetails extends WindowCreator implements CoverageDatabaseController, ActionListener, Disposable {
/**
* Action commands recognized by {@link #actionPerformed(ActionEvent)}.
*/
static final String SELECT_FORMAT="SELECT_FORMAT", SELECT_VARIABLES="SELECT_VARIABLES", OK="OK", CANCEL="CANCEL";
/**
* The {@link CoverageList} that created this panel.
*/
private final CoverageList owner;
/**
* The label showing the filename, including its extension.
*/
private final JTextField filename;
/**
* The format (editable). Contains many {@link String} elements for each existing format
* names, and a single {@link InternationalString} element for the "New format" choice.
*/
private final JComboBox<CharSequence> format;
/**
* The currently selected format. Used only in order to avoid querying the database
* for format twice, and potentially popup two error dialog boxes for the same error.
*/
private String selectedFormat;
/**
* A note (whatever the format is editable or not) about the selected format.
*/
private final JLabel formatNote;
/**
* An item in the {@link #format} combo box with the "New format" label.
* The type is {@link InternationalString} because it is used as a sentinel
* type to be replaced by {@link #defaultFormatName}.
*
* @since 3.16
*/
private final InternationalString newFormat;
/**
* {@code true} if the format describe geophysics values, or {@code false} for packed values.
*/
private final JCheckBox isGeophysics;
/**
* The combo box for horizontal CRS.
*/
private final AuthorityCodesComboBox horizontalCRS;
/**
* The combo box for vertical CRS.
*/
private final AuthorityCodesComboBox verticalCRS;
/**
* The component for the definition of categories.
*/
private final SampleDimensionPanel sampleDimensionEditor;
/**
* The variables selected by the user after the {@link #filterImages} method.
*
* @since 3.15
*/
private transient List<String> selectedVariables;
/**
* The coverage reference in process of being edited, or {@code null} if none.
*/
private transient NewGridCoverageReference reference;
/**
* The default format name of the new {@linkplain #reference}. This is the format to be given
* to {@link NewGridCoverageReference} when the user select "New format" in the combo box.
*
* @since 3.16
*/
private transient String defaultFormatName;
/**
* Creates a new panel.
*
* @param owner The {@link CoverageList} that created this panel.
* @param crsFactory The authority factory to use for fetching the list of available CRS.
*/
@SuppressWarnings("unchecked")
NewGridCoverageDetails(final CoverageList owner, final CRSAuthorityFactory crsFactory) {
this.owner = owner;
setLayout(new BorderLayout());
final Locale locale = getLocale();
final Widgets guires = Widgets.getResources(locale);
final Vocabulary resources = Vocabulary.getResources(locale);
newFormat = new SimpleInternationalString("<html><i>" + resources .getString(Vocabulary.Keys.NewFormat) + "</i></html>");
filename = new JTextField();
format = new JComboBox<>();
formatNote = new JLabel();
isGeophysics = new JCheckBox(guires.getString(Widgets.Keys.RasterIsGeophysics));
horizontalCRS = new AuthorityCodesComboBox(crsFactory, GeographicCRS.class, ProjectedCRS.class);
verticalCRS = new AuthorityCodesComboBox(crsFactory, VerticalCRS.class);
sampleDimensionEditor = new SampleDimensionPanel();
filename.setEditable(false);
format.setEditable(true);
format.setActionCommand(SELECT_FORMAT);
final JXTaskPaneContainer container = new JXTaskPaneContainer();
final GridBagConstraints c = new GridBagConstraints();
c.gridy=0; c.fill=GridBagConstraints.HORIZONTAL;
JXTaskPane pane = new JXTaskPane();
pane.setLayout(new GridBagLayout());
pane.setTitle(resources.getString(Vocabulary.Keys.File));
addRow(pane, resources.getLabel(Vocabulary.Keys.Name), filename, c);
addRow(pane, resources.getLabel(Vocabulary.Keys.Format), format, c);
c.insets.left=6; addRow(pane, null, formatNote, c);
c.insets.left=0; addRow(pane, null, isGeophysics, c);
container.add(pane);
c.gridy=0;
pane = new JXTaskPane();
pane.setLayout(new GridBagLayout());
pane.setTitle(resources.getString(Vocabulary.Keys.CoordinateReferenceSystem));
addRow(pane, resources.getLabel(Vocabulary.Keys.Horizontal), horizontalCRS, c);
addRow(pane, resources.getLabel(Vocabulary.Keys.Vertical), verticalCRS, c);
container.add(pane);
c.gridy=0;
pane = new JXTaskPane();
pane.setTitle(resources.getString(Vocabulary.Keys.SampleDimensions));
pane.add(sampleDimensionEditor);
container.add(pane);
final JButton okButton, cancelButton;
okButton = new JButton(resources.getString(Vocabulary.Keys.Ok));
cancelButton = new JButton(resources.getString(Vocabulary.Keys.Cancel));
okButton.setActionCommand(OK);
cancelButton.setActionCommand(CANCEL);
final JPanel buttonBar = new JPanel(new GridLayout(1, 2));
buttonBar.setOpaque(false);
buttonBar.add(okButton);
buttonBar.add(cancelButton);
// For centering the buttons
final JComponent centered = new JPanel();
centered.setOpaque(false);
centered.add(buttonBar);
add(new JScrollPane(container), BorderLayout.CENTER);
add(centered, BorderLayout.SOUTH);
format .addActionListener(this);
okButton .addActionListener(this);
cancelButton.addActionListener(this);
addAncestorListener(ComponentDisposer.INSTANCE);
}
/**
* Adds a label associated with a field.
*/
private static void addRow(final Container pane, final String text, final JComponent value, final GridBagConstraints c) {
c.gridx=0; c.weightx=0;
if (text != null) {
final JLabel label = new JLabel(text);
label.setLabelFor(value);
pane.add(label, c);
}
c.gridx++; c.weightx=1;
pane.add(value, c);
c.gridy++;
}
/**
* Invoked when the "Ok" or "Cancel" button is pressed, or when a new format is selected,
* or when a new variable is selected.
*/
@Override
public void actionPerformed(final ActionEvent event) {
final String action = event.getActionCommand();
switch (action) {
case SELECT_FORMAT: formatSelected(); break;
case SELECT_VARIABLES: variableSelectionChanged(); break;
case OK: confirm(); break;
case CANCEL: dispose(); break;
}
}
/**
* Returns the name of the currently selected format.
*/
private String getSelectedFormat() {
final Object formatName = format.getSelectedItem();
if (formatName instanceof InternationalString) {
// InternationalString is used as a special value for "New format".
return defaultFormatName;
} else {
return (String) formatName;
}
}
/**
* Invoked when the user selected a format in the {@link JComboBox}, or when the format
* changed programmatically. If the format changed, gets the {@link GridSampleDimension}s
* and updates the field with the new values.
*/
private void formatSelected() {
final String formatName = getSelectedFormat();
if (!Objects.equals(formatName, selectedFormat)) {
selectedFormat = formatName;
List<GridSampleDimension> bands = null;
CoverageStoreException failure = null;
boolean editable = false;
final NewGridCoverageReference reference = this.reference;
if (reference != null) try {
reference.format = formatName;
reference.refresh();
bands = reference.sampleDimensions;
editable = !reference.isFormatDefined();
} catch (CoverageStoreException e) {
failure = e;
}
/*
* Set the SampleDimensionPanel to the new values or clear the panel if there is no
* bands, except if the format does not exist. In the later case, we assume that the
* user wants to create a new format using the current values as a template.
*/
if (!editable || !isNullOrEmpty(bands)) {
boolean geophysics = false;
if (bands != null) {
for (final GridSampleDimension band : bands) {
if (!band.getCategories().isEmpty() && band == band.geophysics(true)) {
geophysics = true;
break;
}
}
}
isGeophysics.setSelected(geophysics);
sampleDimensionEditor.setSampleDimensions(bands);
}
/*
* Sets whatever the format described in the "Sample dimensions" section is
* editable. This also update the note label behind the "Format" field.
*/
formatNote.setText(Widgets.getResources(getLocale()).getString(
editable ? Widgets.Keys.NewFormat : Widgets.Keys.RenameFormatForEdit));
sampleDimensionEditor.setEditable(editable);
isGeophysics.setEnabled(editable);
/*
* Finally, report the error if there is any.
*/
if (failure != null) {
owner.exceptionOccured(failure);
}
}
/*
* Replaces the "{@code <html><i>New Format</i></html>} value in the combo box field
* by the actual value.
*/
if (format.getSelectedItem() instanceof InternationalString) {
format.setSelectedItem(defaultFormatName);
}
}
/**
* Invoked before {@link #coverageAdding} in order to let the user select a variable among
* a list of variables found in the file.
* <p>
* This method needs to be invoked in a thread different than the <cite>Swing</cite> thread.
*
* @param images The variables found in the file.
* @param multiSelectionAllowed {@code true} if the {@link JList} shall allow multi-selection.
* @return The variables selected by the user.
* @throws DatabaseVetoException If the user clicked on the "Cancel" button.
*
* @since 3.15
*/
@Override
public synchronized Collection<String> filterImages(final List<String> images, final boolean multiSelectionAllowed)
throws DatabaseVetoException
{
assert !EventQueue.isDispatchThread();
SwingUtilities.invokeAndWait(new Runnable() {
@Override public void run() {
owner.showVariableChooser(images.toArray(new String[images.size()]), multiSelectionAllowed);
}
});
try {
wait(); // Weakup at the end of actionPerformed(boolean) below.
} catch (InterruptedException e) {
// This happen if the CoverageList frame has been closed
// by CoverageList.Listeners.ancestorRemoved(AncestorEvent).
throw new DatabaseVetoException(e);
}
/*
* At this point, we have been weakup by a button pressed by the user.
* If it was the "Ok" button, the fields are already updated (see the
* actionPerformed method below). If it was the "Cancel" button, then
* the reference has been set to null.
*/
if (selectedVariables == null) {
throw new DatabaseVetoException();
}
return selectedVariables;
}
/**
* Invoked when a new coverage is about to be added. This method set the fields value to
* the values declared in the given {@code reference} argument, and shows the window.
* <p>
* This method needs to be invoked in a thread different than the <cite>Swing</cite> thread.
*
* @throws DatabaseVetoException If the user clicked on the "Cancel" button.
*/
@Override
public synchronized void coverageAdding(final CoverageDatabaseEvent event,
final NewGridCoverageReference newReference) throws DatabaseVetoException
{
assert !EventQueue.isDispatchThread();
/*
* Do not show the widget if this method is invoked after the insertion (because
* it is too late for editing the values), or invoked for record removal.
*/
if (!event.isBefore() || event.getNumEntryChange() <= 0) {
return;
}
/*
* Copies the information from the given reference to the fields in this widget.
*/
final Path file = newReference.getFile();
SwingUtilities.invokeAndWait(new Runnable() {
@Override public void run() {
reference = newReference; // Must be set first.
defaultFormatName = newReference.format;
selectedFormat = null; // Must be before the change of format choices.
try {
final String[] alternatives = newReference.getAlternativeFormats();
final DefaultComboBoxModel<CharSequence> model = new DefaultComboBoxModel<CharSequence>(alternatives);
if (!ArraysExt.contains(alternatives, newReference.format)) {
/*
* InternationalString is used as a sentinal value meaning "New Format",
* to be replaced by NewGridCoverageDetails.this.defaultFormatName when
* needed.
*/
model.insertElementAt(newFormat, 0);
}
format.setModel(model);
} catch (CoverageStoreException e) {
Logging.unexpectedException(null, NewGridCoverageReference.class, "getAlternativeFormats", e);
// Keep the current combo box content unchanged.
}
filename.setText(file.getFileName().toString());
format.setSelectedItem(newReference.format);
setSelectedCode(horizontalCRS, newReference.horizontalSRID);
setSelectedCode(verticalCRS, newReference.verticalSRID);
owner.setSelectionPanel(CoverageList.CONTROLLER);
owner.properties.setImageLater(file);
}
});
try {
wait(); // Weakup at the end of actionPerformed(boolean) below.
} catch (InterruptedException e) {
// This happen if the CoverageList frame has been closed
// by CoverageList.Listeners.ancestorRemoved(AncestorEvent).
throw new DatabaseVetoException(e);
}
/*
* At this point, we have been weakup by a button pressed by the user.
* If it was the "Ok" button, the fields are already updated (see the
* actionPerformed method below). If it was the "Cancel" button, then
* the reference has been set to null.
*/
if (reference == null) {
throw new DatabaseVetoException();
}
}
/**
* Invoked when the user pressed the "Cancel" button or closed the window. When pressing
* the "Cancel" button, this method is invoked by the {@link #actionPerformed(ActionEvent)}
* method. When closing the window, this method is invoked by {@link ComponentDisposer}.
*/
@Override
public synchronized void dispose() {
reference = null;
defaultFormatName = null;
selectedVariables = null;
notifyAll(); // Weakup the sleeping 'coverageAdding' method.
}
/**
* Invoked when the user selected a new variable. This is relevant only for
* files containing different images for different variables.
*/
private synchronized void variableSelectionChanged() {
selectedVariables = owner.getSelectedVariables();
notifyAll(); // Weakup the sleeping 'coverageAdding' method.
}
/**
* Invoked when the user pressed the "Ok" button.
*/
private synchronized void confirm() {
final NewGridCoverageReference reference = this.reference;
try {
reference.format = getSelectedFormat();
reference.horizontalSRID = getSelectedCode(horizontalCRS);
reference.verticalSRID = getSelectedCode(verticalCRS);
reference.sampleDimensions.clear();
sampleDimensionEditor.commitEdit();
final List<GridSampleDimension> bands = sampleDimensionEditor.getSampleDimensions();
if (bands != null) {
final boolean isGeophysics = this.isGeophysics.isSelected();
for (int i=bands.size(); --i>=0;) {
bands.set(i, bands.get(i).geophysics(isGeophysics));
}
reference.sampleDimensions.addAll(bands);
}
} catch (NumberFormatException | ParseException e) {
// Do not weakup the sleeping thread.
// User will need to make an other selection.
return;
}
/*
* Perform some validity checks on user arguments.
*/
if (reference.horizontalSRID == 0) {
incompleteForm(0);
// Do no weakup the sleeping thread.
return;
}
if (reference.verticalSRID == 0 && reference.verticalValues != null) {
incompleteForm(1);
// Do no weakup the sleeping thread.
return;
}
notifyAll(); // Weakup the sleeping 'coverageAdding' method.
}
/**
* Invoked when the widget can not process because of missing information in the form.
*
* @param crsType 0 for horizontal CRS, or 1 for vertical CRS.
*/
private void incompleteForm(final int crsType) {
final Widgets resources = Widgets.getResources(getLocale());
final JXLabel label = new JXLabel(resources.getString(Widgets.Keys.CrsRequired_1, crsType));
final String title = resources.getString(Widgets.Keys.IncompleteForm);
label.setLineWrap(true);
getWindowHandler().showError(this, label, title);
}
/**
* Parses the selected authority code from the given combo box,
* or {@code 0} if there is no selection.
*/
private static int getSelectedCode(final AuthorityCodesComboBox choices) throws NumberFormatException {
String code = choices.getSelectedCode();
if (code != null && !(code = code.trim()).isEmpty()) {
return Integer.parseInt(code);
}
return 0;
}
/**
* Sets the selected authority code to the given combo box.
*/
private static void setSelectedCode(final AuthorityCodesComboBox choices, final int code) {
choices.setSelectedCode(code != 0 ? String.valueOf(code) : null);
}
}