/*
* 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.io.File;
import java.io.IOException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Locale;
import java.util.Arrays;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.Collections;
import java.util.prefs.Preferences;
import java.text.ParseException;
import java.awt.Insets;
import java.awt.Rectangle;
import java.awt.Dimension;
import java.awt.Component;
import java.awt.GridLayout;
import java.awt.BorderLayout;
import java.awt.GridBagLayout;
import java.awt.GridBagConstraints;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import javax.imageio.spi.ImageReaderSpi;
import javax.swing.*;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import javax.swing.event.TableModelEvent;
import javax.swing.event.TableModelListener;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener;
import org.jdesktop.swingx.JXTitledPanel;
import org.apache.sis.util.ArraysExt;
import org.geotoolkit.resources.Vocabulary;
import org.geotoolkit.coverage.grid.ImageGeometry;
import org.geotoolkit.image.io.mosaic.TileManager;
import org.geotoolkit.image.io.mosaic.MosaicBuilder;
import org.geotoolkit.image.io.ImageReaderAdapter;
import org.geotoolkit.gui.swing.Dialog;
import org.geotoolkit.gui.swing.ListTableModel;
import org.geotoolkit.internal.swing.SwingUtilities;
import org.geotoolkit.internal.swing.FileField;
import org.geotoolkit.internal.swing.SizeFields;
import org.geotoolkit.internal.swing.table.LabeledRenderer;
import static org.geotoolkit.gui.swing.image.MosaicChooser.OUTPUT_FORMAT;
import static org.geotoolkit.gui.swing.image.MosaicChooser.OUTPUT_DIRECTORY;
/**
* Configures a {@link MosaicBuilder} according the input provided by a user. The caller can
* invoke one of the one-argument constructors (optional but recommended) in order to initialize
* the widgets with a set of default values. After the widget has been displayed, the caller can
* invoke {@link #getTileManager()} in order to get the user's choices in an object ready for use.
* <p>
* <b>Example:</b>
*
* {@preformat java
* MosaicBuilderEditor editor = new MosaicBuilderEditor(boundsOfTheWholeMosaic);
* if (editor.showDialog(null, "Define pyramid tiling")) {
* TileManager mosaic = editor.getTileManager();
* // Process here.
* }
* }
*
* @author Martin Desruisseaux (Geomatys)
* @version 3.12
*
* @since 3.00
* @module
*/
@SuppressWarnings("serial")
public class MosaicBuilderEditor extends JComponent implements MosaicPerformanceGraph.Delayed, Dialog {
/**
* The default tile size. If {@link MosaicBuilder} can not suggest a tile size,
* we will use the size specified by the WMTS (<cite>Web Map Tile Service</cite>)
* specification.
*/
private static final Dimension DEFAULT_TILE_SIZE = new Dimension(256, 256);
/**
* The delay before to plot the graph, in milliseconds.
*/
private static final int DELAY = 1000;
/**
* The mosaic builder to configure. This is the instance given to the constructor.
* This builder may not be synchronized with the content of this widget - the
* synchronization happen only when {@link #getMosaicBuilder()} is invoked.
*/
private final MosaicBuilder builder;
/**
* The table model for the subsampling selection.
*/
private final Subsamplings subsamplingTable;
/**
* The size of output tiles.
*/
private final SizeFields sizeFields;
/**
* The target file format for writing tiles.
*/
private final JComboBox<ImageFormatEntry> formatChoices;
/**
* The output directory.
*/
private final FileField directoryField;
/**
* A plot of the estimated cost of loading tiles at given resolution.
*/
private final MosaicPerformanceGraph plot;
/**
* The progress during the calculation of the performance graph.
*/
private final JProgressBar progressBar;
/**
* Creates a new panel for configuring a default mosaic builder.
*/
public MosaicBuilderEditor() {
this(new MosaicBuilder());
}
/**
* Creates a new panel suitable for tiles in a mosaic of the given size.
*
* @param bounds The bounds of the whole mosaic.
*/
public MosaicBuilderEditor(final Rectangle bounds) {
this(create(bounds));
}
/**
* Work around for RFE #4093999 in Sun's bug database
* ("Relax constraint on placement of this()/super() call in constructors").
*/
private static MosaicBuilder create(final Rectangle bounds) {
final MosaicBuilder builder = new MosaicBuilder();
builder.setUntiledImageBounds(bounds);
return builder;
}
/**
* Creates a new panel suitable for the given tiles, specified as {@code TileManager} objects.
* Only one tile manager is usually provided. However more managers can be provided if, for
* example, {@link org.geotoolkit.image.io.mosaic.TileManagerFactory} failed to create only
* one instance from a set of tiles.
*
* @param managers The tiles for which to setup default values.
* @throws IOException If an I/O operation was necessary and failed.
*/
public MosaicBuilderEditor(final TileManager... managers) throws IOException {
this(bounds(managers));
}
/**
* Creates a new panel for configuring the given mosaic builder.
*
* @param builder The mosaic builder to be configured by this panel.
*/
public MosaicBuilderEditor(final MosaicBuilder builder) {
this.builder = builder;
final Locale locale = getLocale();
final Vocabulary resources = Vocabulary.getResources(locale);
/*
* Determines the default values. We fetch the values from the MosaicBuilder
* if they are defined, or from the user's preferences otherwise.
*/
Path directory;
Dimension tileSize, minSize;
final String preferredFormat;
final Dimension[] subsamplings;
/* Block for reducing variable scope */ {
final ImageReaderSpi reader;
final Preferences prefs = Preferences.userNodeForPackage(MosaicBuilderEditor.class);
synchronized (builder) {
reader = builder.getTileReaderSpi();
directory = builder.getTileDirectory();
subsamplings = builder.getSubsamplings();
tileSize = builder.getTileSize();
}
if (reader != null) {
// FormatNames can not be a null or empty array according the method contract.
preferredFormat = reader.getFormatNames()[0];
} else {
preferredFormat = prefs.get(OUTPUT_FORMAT, "png");
}
if (directory == null) {
directory = Paths.get(prefs.get(OUTPUT_DIRECTORY, System.getProperty("user.home", ".")));
}
if (tileSize == null) {
tileSize = DEFAULT_TILE_SIZE;
}
/*
* A minimal size is essential, because too small size will cause too many tiles
* to be created, which cause a OutOfMemoryError.
*/
minSize = new Dimension(256, 256);
}
/*
* The table where to specifies subsampling, together with a "Remove" botton for
* removing rows. There is no "add" button given that subsampling can be added on
* the last row.
*/
subsamplingTable = new Subsamplings(resources);
subsamplingTable.setElements(subsamplings);
final JTable subsamplingTable = new JTable(this.subsamplingTable);
subsamplingTable.setDefaultRenderer(Integer.class, new LabeledRenderer.Numeric(locale, true));
final JButton removeButton = new JButton(resources.getString(Vocabulary.Keys.Remove));
removeButton.setEnabled(false);
JPanel subsamplingPane = new JPanel(new BorderLayout());
subsamplingPane.add(new JScrollPane(subsamplingTable), BorderLayout.CENTER);
subsamplingPane.add(removeButton, BorderLayout.SOUTH);
subsamplingPane = new JXTitledPanel(resources.getString(Vocabulary.Keys.Subsampling), subsamplingPane);
/*
* The panel where to select the tile size, file format and the output directory.
*/
sizeFields = new SizeFields(locale, DEFAULT_TILE_SIZE, minSize);
sizeFields.setSizeValue(tileSize);
formatChoices = ImageFormatEntry.comboBox(preferredFormat);
final JPanel formatPanel = new JPanel(new BorderLayout());
formatPanel.add(formatChoices, BorderLayout.CENTER);
formatPanel.setBorder(BorderFactory.createTitledBorder(resources.getString(Vocabulary.Keys.Format)));
directoryField = new FileField(locale, null, true);
directoryField.setFile(directory.toFile());
directoryField.setBorder(BorderFactory.createTitledBorder(resources.getString(Vocabulary.Keys.OutputDirectory)));
final JLabel explain = new JLabel(); // No purpose other than fill space at this time.
/*
* Assembles the control panel which is on the right side of the subsampling table.
*/
final JPanel controlPane = new JPanel(new GridBagLayout());
final GridBagConstraints c = new GridBagConstraints();
c.insets.bottom=9;
c.weightx=1; c.fill=GridBagConstraints.HORIZONTAL;
c.gridx=0; c.anchor=GridBagConstraints.LINE_START;
c.gridy=0; controlPane.add(sizeFields, c);
c.gridy++; controlPane.add(formatPanel, c);
c.gridy++; controlPane.add(directoryField, c); c.weighty=1; c.fill=GridBagConstraints.BOTH;
c.gridy++; controlPane.add(explain, c);
/*
* Creates the panel which will contains the plot of estimated performance.
*/
plot = new MosaicPerformanceGraph();
plot.setMargin(new Insets(15, 50, 45, 15));
progressBar = new JProgressBar();
progressBar.setEnabled(false);
plot.setProgressBar(progressBar);
final JPanel plotPanel = new JPanel(new BorderLayout());
plotPanel.add(plot, BorderLayout.CENTER);
plotPanel.add(progressBar, BorderLayout.SOUTH);
/*
* Layout all the above components. The divider location has been determined
* empirically for allowing the subsamplings columns to be fully visible.
*/
final JPanel panel = new JPanel(new GridLayout(1, 2, 15, 9));
panel.add(subsamplingPane);
panel.add(controlPane);
final JSplitPane sp = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, true, panel, plotPanel);
sp.setDividerLocation(400);
sp.setBorder(null);
setLayout(new BorderLayout());
add(sp, BorderLayout.CENTER);
setPreferredSize(new Dimension(800, 300));
/*
* Adds listeners to be notified when a property that may affect the MosaicBuilder
* changed. They will trig a repaint of the graph of estimated tiles loading efficiency.
* Defines also listeners for controlling the removal of rows in the subsampling table.
*/
final class Listener implements TableModelListener, ChangeListener, ActionListener, ListSelectionListener {
/** Invoked when a subsampling value in the table has been edited. */
@Override public void tableChanged(final TableModelEvent event) {
plotEfficiency(DELAY);
}
/** Invoked when a tile size (width or height) value changed. */
@Override public void stateChanged(final ChangeEvent event) {
plotEfficiency(DELAY);
}
/** Invoked when the "Remove" button is pressed. */
@Override public void actionPerformed(final ActionEvent event) {
// The insertion row is not a "real" row and must be omitted.
final int insertionRow = subsamplingTable.getModel().getRowCount() - 1;
int[] selected = subsamplingTable.getSelectedRows();
int count = 0;
for (int i=0; i<selected.length; i++) {
final int row = selected[i];
if (row < insertionRow) {
selected[count++] = row;
}
}
selected = ArraysExt.resize(selected, count);
((Subsamplings) subsamplingTable.getModel()).remove(selected);
}
/** Invoked when the row selection in the subsampling table changed. */
@Override public void valueChanged(final ListSelectionEvent event) {
final int min = ((ListSelectionModel) event.getSource()).getMinSelectionIndex();
removeButton.setEnabled(min >= 0 && min < subsamplingTable.getModel().getRowCount() - 1);
}
}
final Listener listener = new Listener();
this.subsamplingTable.addTableModelListener(listener);
this.sizeFields.addChangeListener(listener);
removeButton.addActionListener(listener);
subsamplingTable.getSelectionModel().addListSelectionListener(listener);
plotEfficiency(0);
}
/**
* Proposes default values suitable for the given tiles, specified as {@code TileManager}
* objects. Only one tile manager is usually provided. However more managers can be provided
* if, for example, {@link org.geotoolkit.image.io.mosaic.TileManagerFactory} failed to create
* only one instance from a set of tiles.
*
* @param managers The tiles for which to setup default values.
* @throws IOException If an I/O operation was necessary and failed.
*/
public void initializeForTiles(final TileManager... managers) throws IOException {
initializeForBounds(bounds(managers));
}
/**
* Searches for a rectangle that encompass every tiles.
*/
private static Rectangle bounds(final TileManager... managers) throws IOException {
Rectangle bounds = null;
for (final TileManager manager : managers) {
final ImageGeometry geom = manager.getGridGeometry();
if (geom != null) {
final Rectangle candidate = geom.getExtent();
if (bounds == null) {
bounds = candidate;
} else {
bounds.add(candidate);
}
}
}
return bounds;
}
/**
* Proposes default values suitable for tiles in a mosaic of the given size.
*
* @param bounds The bounds of the whole mosaic.
*/
public void initializeForBounds(final Rectangle bounds) {
synchronized (getTreeLock()) {
Dimension tileSize;
final Dimension[] subsamplings;
final MosaicBuilder builder = this.builder;
synchronized (builder) {
/*
* If a region was found, discard the values previously set and give the new region
* to the TileBuilder. Then asks for the default values proposed by the builder
*/
if (bounds != null) {
builder.setTileSize(null);
builder.setSubsamplings((Dimension[]) null);
builder.setUntiledImageBounds(bounds);
}
subsamplings = builder.getSubsamplings();
tileSize = builder.getTileSize();
}
if (tileSize == null) {
tileSize = DEFAULT_TILE_SIZE;
}
sizeFields.setSizeValue(tileSize);
subsamplingTable.setElements(subsamplings);
}
}
/**
* The table model for the subsamplings table.
*
* @author Martin Desruisseaux (Geomatys)
* @version 3.00
*
* @since 3.00
* @module
*/
private static final class Subsamplings extends ListTableModel<Dimension> implements Comparator<Dimension> {
/**
* For cross-version compatibility.
*/
private static final long serialVersionUID = 4366921097769025343L;
/**
* Localized column titles.
*/
private final String[] titles;
/**
* Creates a default set of subsampling values.
*/
Subsamplings(final Vocabulary resources) {
super(Dimension.class, new ArrayList<Dimension>());
titles = new String[] {
resources.getString(Vocabulary.Keys.Level),
resources.getString(Vocabulary.Keys.Axis_1, "x"),
resources.getString(Vocabulary.Keys.Axis_1, "y")
};
Collections.sort(elements, this);
}
/**
* Returns the square of the area of a rectangle of the given size.
*/
private static long areaSquared(final Dimension size) {
long s;
return ((s = size.width) * s) + ((s = size.height) * s);
}
/**
* Compares the given subsamplings for order. This is used for keeping the
* subsampling list in increasing order.
*/
@Override
public int compare(final Dimension s1, final Dimension s2) {
return Long.signum(areaSquared(s1) - areaSquared(s2));
}
/**
* Replaces all current values by the given ones.
*/
@Override
public void setElements(final Dimension... sub) {
elements.clear();
if (sub != null) {
elements.addAll(Arrays.asList(sub));
} else {
elements.add(new Dimension(1,1));
}
fireTableDataChanged();
}
/**
* Overrides the method inherited from the subclass in order to execute it from the
* current thread rather than the Swing thread. This is required in order to avoid
* deadlock. Should be okay since this method is invoked inside a block synchronized
* on the AWT tree lock.
*/
@Override
public Dimension[] getElements() {
return elements.toArray(new Dimension[elements.size()]);
}
/**
* Returns the number of row, including the insertion row.
*/
@Override
public int getRowCount() {
return elements.size() + 1;
}
/**
* Returns the number of columns, which is 3 including the header column.
*/
@Override
public int getColumnCount() {
return titles.length;
}
/**
* Returns the name of the given column.
*/
@Override
public String getColumnName(final int column) {
return titles[column];
}
/**
* Returns {@code Integer.class} regardless of the column index.
*/
@Override
public Class<Integer> getColumnClass(final int columnIndex) {
return Integer.class;
}
/**
* Returns {@code true} for the columns that are not the header columns.
*/
@Override
public boolean isCellEditable(final int rowIndex, final int columnIndex) {
return columnIndex != 0;
}
/**
* Returns the value in the given cell.
*/
@Override
public Object getValueAt(final int rowIndex, final int columnIndex) {
if (columnIndex == 0) {
return rowIndex + 1;
}
if (rowIndex < elements.size()) {
final Dimension size = elements.get(rowIndex);
switch (columnIndex) {
case 1: return size.width;
case 2: return size.height;
}
}
return null; // Insertion row.
}
/**
* Sets the value in the given cell. If the value is added in the insertion row,
* the same value is added for both <var>x</var> and <var>y</var> axes, which is
* usually the desired behavior.
*/
@Override
public void setValueAt(final Object value, final int rowIndex, final int columnIndex) {
if (value != null) {
final Dimension s;
final int n = (Integer) value;
if (rowIndex < elements.size()) {
s = elements.get(rowIndex);
switch (columnIndex) {
case 1: if (s.width == n) return; else s.width = n; break;
case 2: if (s.height == n) return; else s.height = n; break;
}
} else {
s = new Dimension(n, n);
elements.add(s);
}
/*
* Sorts the subsamplings in increasing order and fires a change event for the
* whole table only if the position of the new subsampling changed as a result
* of this operation. We test only the new subsampling because the other ones
* are already sorted, so they should not move if the edited record did not moved.
*/
Collections.sort(elements, this);
if (elements.get(rowIndex) == s) {
fireTableCellUpdated(rowIndex, columnIndex);
} else {
fireTableRowsUpdated(0, elements.size());
}
}
}
}
/**
* Refreshes the plot of estimated efficiency. This method is invoked automatically when
* the values of some fields changed. The default implementation starts the calculation
* in a background thread.
*
* @param delay How long to wait (in milliseconds) before to perform the calculation.
*/
protected void plotEfficiency(final long delay) {
plot.plotLater(null, this, delay);
}
/**
* Configures the {@code MosaicBuilder} with the informations provided by the user
* and return it.
*
* {@note Use this method when the widget state will not change anymore. If the user is still
* editing the values in the widget, then invoking <code>getTileManager()</code> is preferable
* than <code>getTileBuilder().getTileManager()</code> for synchronization reasons.}
*
* @return The configured mosaic builder.
* @throws IOException if an I/O operation was required and failed.
*/
public MosaicBuilder getMosaicBuilder() throws IOException {
getTileManager(false);
return builder;
}
/**
* Configures the {@code MosaicBuilder} with the informations provided by the user
* and create the mosaic. This method is automatically invoked when a graph is about to be
* plot. It can also be invoked directly by the user, but may block if the builder is
* currently in use by an other thread.
*
* @return The selected tiles as a {@code TileManager} object.
* @throws IOException if an I/O operation was required and failed.
*/
@Override
public TileManager getTileManager() throws IOException {
return getTileManager(true);
}
/**
* Implementation of {@link #getTileManager} when the last step (the invocation of
* {@link MosaicBuilder#createTileManager()}) is disabled if {@code run} is {@code false}.
* This method exists because we want {@code builder.createTileManager()} to be invoked in
* the same synchronization block than the one that configured the builder.
*/
private TileManager getTileManager(final boolean run) throws IOException {
final File directory;
final Dimension tileSize;
final Dimension[] subsamplings;
final ImageFormatEntry tileFormat;
synchronized (getTreeLock()) {
directory = directoryField.getFile();
tileFormat = (ImageFormatEntry) formatChoices.getSelectedItem();
tileSize = sizeFields.getSizeValue();
subsamplings = subsamplingTable.getElements();
}
final Preferences prefs = Preferences.userNodeForPackage(MosaicBuilderEditor.class);
prefs.put(OUTPUT_FORMAT, tileFormat.getFormat());
prefs.put(OUTPUT_DIRECTORY, directory.getPath());
final MosaicBuilder builder = this.builder;
synchronized (builder) {
builder.setTileDirectory(directory);
builder.setTileSize(tileSize);
builder.setSubsamplings(subsamplings);
builder.setTileReaderSpi(ImageReaderAdapter.Spi.unwrap(tileFormat.getReader()));
return run ? builder.createTileManager() : null;
}
}
/**
* Notifies that a {@link TileManager} has been created from the parameter edited in this widget.
* This method is invoked automatically after the fields in this widget has been edited.
* It can also be invoked directly by the user. Current implementation does nothing, but
* subclasses can override this method for remembering the {@code TileManager}.
*
* @param mosaic The mosaic created from the information provided in this widget,
* or {@code null} if the {@code TileManager} creation has been canceled
* before completion.
*/
@Override
public void done(TileManager mosaic) {
}
/**
* Notifies that the creation of a {@link TileManager} failed with the given exception. This
* method is invoked instead than {@link #done(TileManager)} if an exception occurred during
* the execution of {@link MosaicPerformanceGraph#plotEfficiency(String, TileManager)}.
* <p>
* The default implementation does nothing. Subclasses can override this method in order
* to report the error in the way that best suite their application.
*
* @param exception The exception which occurred.
*/
@Override
public void failed(Throwable exception) {
}
/**
* {@inheritDoc}
*
* @since 3.12
*/
@Override
public void commitEdit() throws ParseException {
}
/**
* {@inheritDoc}
*/
@Override
public boolean showDialog(final Component owner, final String title) {
return SwingUtilities.showDialog(owner, this, title);
}
}