/*
* 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.image.io.mosaic;
import java.io.*;
import java.awt.*;
import javax.swing.*;
import java.awt.event.MouseEvent;
import java.awt.event.MouseAdapter;
import java.awt.image.RenderedImage;
import java.awt.geom.AffineTransform;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.util.concurrent.ExecutionException;
import java.util.logging.Level;
import java.util.Collection;
import javax.imageio.ImageIO;
import org.geotoolkit.util.Exceptions;
import org.apache.sis.util.logging.Logging;
import org.geotoolkit.internal.GraphicsUtilities;
import static java.lang.StrictMath.*;
/**
* Displays the image read by {@link MosaicImageReader}. This is used only for visual testing
* of the {@link org.geotoolkit.image.io.mosaic} package. This is <strong>not</strong> intended
* to be a building block of any application, since this class is keep simple on intend. For
* example the image is reloaded every time the component is paint, which is very inefficient
* but appropriate for the purpose of testing the image reader.
*
* @author Martin Desruisseaux (Geomatys)
* @version 3.00
*
* @since 3.00
*/
@SuppressWarnings("serial")
public final strictfp class MosaicImageViewer extends JPanel implements ChangeListener {
/**
* Initial size for the canvas. Used only at construction time;
* user can resize the window after it has been made visible.
*/
private static final int INITIAL_WIDTH = 800, INITIAL_HEIGHT = 600;
/**
* Property name for {@link PropertyChangeEvent}.
*/
private static final String X_SUBSAMPLING = "xSubsampling", Y_SUBSAMPLING = "ySubsampling";
/**
* The tile manager.
*/
private final TileManager tiles;
/**
* The image reader.
*/
private final MosaicImageReader reader;
/**
* The last {@link RenderedImage} read, or an instance of {@link Exception}
* if an error occurred, or {@code null} if a reading is under progress.
*/
private transient Object image;
/**
* The image bounds.
*/
private final Rectangle bounds;
/**
* The source image area used last time the image has been painted.
* This is computed from ({@link #centerX}, {@link #centerY}) and
* the subsampling.
*/
private final Rectangle view;
/**
* The transform from image to display.
*/
private final AffineTransform displayToImage;
/**
* The image to show when the region to read is empty.
*/
private final RenderedImage empty;
/**
* Coordinates of the image pixel to put at the center of the widget area.
*/
private int centerX, centerY;
/**
* Subsampling along X and Y axes.
*/
private int xSubsampling, ySubsampling;
/**
* Options
*/
private boolean synchronizeXY=true, subsamplingChangeAllowed=true;
/**
* {@code true} while the two spinners are adjusted to the same value.
*/
private transient boolean isSynchronizing;
/**
* Creates a panel for displaying the image read from the given tiles. This constructor
* creates only the image view area. In order to get a panel including the controller,
* invoke {@link #createControlPanel}.
*
* @param tiles The tiles to display.
* @throws IOException if an I/O operation was required and failed.
*/
protected MosaicImageViewer(final TileManager tiles) throws IOException {
this.tiles = tiles;
bounds = tiles.getGridGeometry().getExtent();
view = new Rectangle(bounds);
reader = new MosaicImageReader();
reader.setInput(tiles);
displayToImage = new AffineTransform();
centerX = bounds.x + bounds.width / 2;
centerY = bounds.y + bounds.height / 2;
xSubsampling = ySubsampling = min(bounds.width/INITIAL_WIDTH, bounds.height/INITIAL_HEIGHT);
addMouseListener(new MouseAdapter() {
@Override public void mouseClicked(final MouseEvent event) {
final Point point = event.getPoint();
displayToImage.transform(point, point);
centerX = point.x;
centerY = point.y;
repaint();
}
});
try (InputStream stop = ClassLoader.getSystemResourceAsStream("toolbarButtonGraphics/general/Stop24.gif")) {
empty = ImageIO.read(stop);
}
}
/**
* Reads the image and paints it. The image is read again every time the component
* needs to be repainted. This is inefficient, but the purpose of this class is
* really to test {@link MosaicImageReader}...
*
* @param graphics The graphics where to paint the image.
*/
@Override
protected void paintComponent(final Graphics graphics) {
super.paintComponent(graphics);
final Graphics2D gr = (Graphics2D) graphics;
final int width = getWidth();
final int height = getHeight();
final Object toShow = image;
image = null;
if (toShow == null) {
view.width = width * xSubsampling;
view.height = height * ySubsampling;
view.x = max(bounds.x, centerX - view.width / 2);
view.y = max(bounds.y, centerY - view.height / 2);
final Rectangle region = view.intersection(bounds);
if (view.width > region.width) {
view.x -= (view.width - region.width) / 2;
}
if (view.height > region.height) {
view.y -= (view.height - region.height) / 2;
}
if (region.isEmpty()) {
image = empty;
return;
}
final MosaicImageReadParam param = reader.getDefaultReadParam();
param.setSourceSubsampling(xSubsampling, ySubsampling, 0, 0);
param.setSubsamplingChangeAllowed(subsamplingChangeAllowed);
param.setSourceRegion(region);
final SwingWorker<Object,Object> worker = new SwingWorker<Object,Object>() {
/**
* Loads the image in a background thread.
*/
@Override protected Object doInBackground() throws IOException {
return reader.readAsRenderedImage(0, param);
}
/**
* Invoked after the loading is done. Note that the old values of x/y subsamplings
* are actually unknown, because they may have been changed by the user.
*/
@Override protected void done() {
try {
image = get();
xSubsampling = param.getSourceXSubsampling();
ySubsampling = param.getSourceYSubsampling();
MosaicImageViewer.this.firePropertyChange(X_SUBSAMPLING, null, xSubsampling);
MosaicImageViewer.this.firePropertyChange(Y_SUBSAMPLING, null, ySubsampling);
} catch (ExecutionException exception) {
image = exception.getCause();
} catch (InterruptedException exception) {
image = exception;
}
repaint();
}
};
image = param; // Just a flag saying that the reader is busy.
worker.execute();
} else {
if (toShow instanceof RenderedImage) {
final RenderedImage image = (RenderedImage) toShow;
final int dx = (width - image.getWidth()) / 2;
final int dy = (height - image.getHeight()) / 2;
displayToImage.setToTranslation(dx, dy);
gr.drawRenderedImage(image, displayToImage);
displayToImage.setToTranslation(view.x, view.y);
displayToImage.scale(xSubsampling, ySubsampling);
displayToImage.translate(-dx, -dy);
return;
}
if (toShow instanceof Throwable) {
Exceptions.paintStackTrace(gr, getBounds(), (Throwable) toShow);
return;
}
}
gr.drawString("Loading...", width/2, height/2);
}
/**
* Invoked when the subsampling changed. Users should never invoke this method directly since
* this is implementation details.
*
* @param event The change event.
*/
@Override
public void stateChanged(final ChangeEvent event) {
final JSpinner source = (JSpinner) event.getSource();
final String name = source.getName();
final int value = (Integer) source.getValue();
if (X_SUBSAMPLING.equals(name)) xSubsampling = value;
if (Y_SUBSAMPLING.equals(name)) ySubsampling = value;
repaint();
}
/**
* Creates a control panel with this image panel and its controllers.
*
* @return The control panel.
* @throws IOException If an I/O operation was required and failed.
*/
protected JComponent createControlPanel() throws IOException {
final JPanel panel = new JPanel(new BorderLayout());
panel.add(this, BorderLayout.CENTER);
final JPanel subsamplings = new JPanel(new GridLayout(3,3));
subsamplings.add(new JLabel());
subsamplings.add(new JLabel("Requested", JLabel.CENTER));
subsamplings.add(new JLabel("Actual", JLabel.CENTER));
subsamplings.setBorder(BorderFactory.createTitledBorder("Subsampling"));
final JSpinner x = addControls(subsamplings, true);
final JSpinner y = addControls(subsamplings, false);
synchronize(x, y);
synchronize(y, x);
final Box options = Box.createVerticalBox();
final JCheckBox sync = new JCheckBox("Synchronize X and Y subsamplings", synchronizeXY);
final JCheckBox alch = new JCheckBox("Allow subsampling changes", subsamplingChangeAllowed);
sync.addChangeListener(new ChangeListener() {
@Override public void stateChanged(final ChangeEvent event) {
synchronizeXY = sync.isSelected();
}
});
alch.addChangeListener(new ChangeListener() {
@Override public void stateChanged(final ChangeEvent event) {
subsamplingChangeAllowed = alch.isSelected();
}
});
options.add(sync);
options.add(alch);
options.setBorder(BorderFactory.createTitledBorder("Options"));
panel.add(options, BorderLayout.EAST);
final JPanel controls = new JPanel(new BorderLayout());
controls.add(subsamplings, BorderLayout.WEST);
controls.add(options, BorderLayout.EAST);
controls.setBorder(BorderFactory.createEmptyBorder(6, 9, 9, 9));
panel.add(controls, BorderLayout.SOUTH);
panel.setOpaque(false);
final JTabbedPane tabs = new JTabbedPane();
tabs.addTab("Image", panel);
if (tiles instanceof TreeTileManager) {
final TreeTileManagerViewer intern = new TreeTileManagerViewer((TreeTileManager) tiles);
tabs.addTab("TreeTileManager", intern.createControlPanel());
}
return tabs;
}
/**
* Adds the label and controls for one axis.
*/
private JSpinner addControls(final Container subsamplings, final boolean isX) {
final String label, property;
final int subsampling;
if (isX) {
label = "X:";
property = X_SUBSAMPLING;
subsampling = xSubsampling;
} else {
label = "Y:";
property = Y_SUBSAMPLING;
subsampling = ySubsampling;
}
final JSpinner request = new JSpinner();
final SpinnerNumberModel model = (SpinnerNumberModel) request.getModel();
model.setMinimum(1);
model.setValue(subsampling);
request.setName(property);
request.addChangeListener(this);
final JTextField actual = new JTextField(model.getValue().toString());
addPropertyChangeListener(property, new PropertyChangeListener() {
@Override public void propertyChange(final PropertyChangeEvent event) {
actual.setText(String.valueOf(event.getNewValue()));
}
});
actual.setEditable(false);
actual.setHorizontalAlignment(JTextField.RIGHT);
subsamplings.add(new JLabel(label, JLabel.CENTER));
subsamplings.add(request);
subsamplings.add(actual);
return request;
}
/**
* Sets {@code target} to the same value than {@code source} when the later changed.
*/
private void synchronize(final JSpinner source, final JSpinner target) {
source.addChangeListener(new ChangeListener() {
@Override public void stateChanged(final ChangeEvent event) {
if (synchronizeXY && !isSynchronizing) {
isSynchronizing = true;
target.setValue(source.getValue());
isSynchronizing = false;
}
}
});
}
/**
* Displays this viewer in a frame. This is a convenience method only.
*
* @throws IOException If an I/O operation was required and failed.
*/
public void showInFrame() throws IOException {
final JFrame frame = new JFrame("MosaicImageReader");
frame.add(createControlPanel());
frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
frame.setSize(INITIAL_WIDTH, INITIAL_HEIGHT);
frame.setLocationByPlatform(true);
frame.setVisible(true);
}
/**
* Loads a serialized {@link TileManager} and display it.
*
* @param args A single command-line argument which is the name of the serialized mosaic.
* @throws IOException If the tiles can not be deserialized.
* @throws ClassNotFoundException If the serialized stream contains an unknown class.
*/
public static void main(final String[] args) throws IOException, ClassNotFoundException {
final Object tiles;
try (ObjectInputStream in = new ObjectInputStream(new BufferedInputStream(new FileInputStream(args[0])))) {
tiles = in.readObject();
}
final TileManager manager;
if (tiles instanceof TileManager) {
manager = (TileManager) tiles;
} else if (tiles instanceof Tile[]) {
manager = TileManagerFactory.DEFAULT.create((Tile[]) tiles)[0];
} else if (tiles instanceof Collection<?>) {
@SuppressWarnings({"unchecked","rawtypes"})
final Collection<Tile> c = (Collection) tiles;
manager = TileManagerFactory.DEFAULT.create(c)[0];
} else {
System.out.println("Unsupported type: " + tiles.getClass());
return;
}
GraphicsUtilities.setLookAndFeel(MosaicImageViewer.class, "main");
final MosaicImageViewer viewer = new MosaicImageViewer(manager);
viewer.showInFrame();
}
}