/*
* Geotoolkit.org - An Open Source Java GIS Toolkit
* http://www.geotoolkit.org
*
* (C) 2003-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.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.GridLayout;
import java.awt.Paint;
import java.awt.geom.Rectangle2D;
import java.awt.geom.AffineTransform;
import java.awt.image.RenderedImage;
import java.awt.image.renderable.RenderableImage;
import javax.media.jai.operator.ScaleDescriptor;
import java.util.Locale;
import java.util.concurrent.Future;
import java.util.concurrent.ExecutionException;
import javax.swing.BorderFactory;
import javax.swing.JComponent;
import javax.swing.JLabel;
import javax.swing.JProgressBar;
import javax.swing.SwingWorker;
import org.geotoolkit.util.Exceptions;
import org.geotoolkit.gui.swing.ZoomPane;
import org.geotoolkit.image.internal.ImageUtilities;
import org.geotoolkit.resources.Vocabulary;
/**
* A simple image viewer. This widget accepts either {@linkplain RenderedImage rendered} or
* {@linkplain RenderableImage renderable} image. Rendered image are displayed immediately,
* while renderable image will be rendered in a background thread when first requested.
* <p>
* This widget may scale down images for faster rendering. This is convenient for image
* previews, but should not be used as a "real" (i.e. robust and accurate) renderer.
*
* @author Martin Desruisseaux (IRD, Geomatys)
* @version 3.07
*
* @see ImageProperties
*
* @since 2.3
* @module
*/
@SuppressWarnings("serial")
public class ImagePane extends ZoomPane {
/**
* The space to insert between the border of this component and the progress bar, if any.
*/
private static final int MARGIN = 24;
/**
* The default size for rendered image produced by a {@link RenderableImage}.
* This is also the maximum size for a {@link RenderedImage}; bigger image
* will be scaled down for faster rendering.
*/
private final Dimension renderedSize;
/**
* The renderable image, or {@code null} if none. If non-null, then the {@code Render}
* will transform this renderable image into a rendered one when first requested.
* Once the image is rendered, this field is set to {@code null}.
*/
private RenderableImage renderable;
/**
* The rendered image, or {@code null} if none. This image may be explicitly set
* by {@link #setImage(RenderedImage)}, or computed by {@code Render}.
*/
private RenderedImage rendered;
/**
* If the rendering failed, the exception to paint in place of the image.
*
* @since 3.05
*/
private Throwable error;
/**
* The progress pane (including a label and a progress bar), or {@code null} if none.
* Will be created only when {@link #getProgressPane()} is first invoked.
*/
private ProgressPane progressPane;
/**
* The task which is rendering a {@link RenderableImage} in a background thread.
* This field shall be read and write from the <cite>Swing</cite> thread only.
*/
private transient Future<RenderedImage> render;
/**
* Constructs an initially empty image pane with a default rendered image size.
*/
public ImagePane() {
this(512);
}
/**
* Constructs an initially empty image pane with the specified rendered image size.
* The {@code renderedSize} argument is the <em>maximum</em> width and height for
* {@linkplain RenderedImage rendered image}. Images greater than this value will be
* scaled down for faster rendering.
*
* @param renderedSize The maximal image width and height.
*/
public ImagePane(final int renderedSize) {
this(new Dimension(renderedSize, renderedSize));
}
/**
* Constructs an initially empty image pane with the specified rendered image size.
* The {@code renderedSize} argument is the <em>maximum</em> dimension for
* {@linkplain RenderedImage rendered image}. Images greater than this value will be
* scaled down for faster rendering.
*
* @param renderedSize The maximal image dimension before to scale down.
*
* @since 3.07
*/
public ImagePane(final Dimension renderedSize) {
super(UNIFORM_SCALE | TRANSLATE_X | TRANSLATE_Y | ROTATE | RESET | DEFAULT_ZOOM);
setResetPolicy(true);
this.renderedSize = new Dimension(renderedSize);
}
/**
* Cancel computation tasks and reset all fields to {@code null}.
* This is invoked when the user specify a different image.
*
* @return The old renderable or rendered image.
*/
private Object clear() {
Object old = renderable;
if (old == null) {
old = rendered;
}
if (render != null) {
render.cancel(true);
render = null;
}
renderable = null;
rendered = null;
error = null;
return old;
}
/**
* Sets the source renderable image. The given image will be
* {@linkplain RenderableImage#createDefaultRendering() rendered}
* in a background thread when first needed.
*
* @param image The image to display, or {@code null} if none.
*/
public void setImage(final RenderableImage image) {
final Throwable error = this.error;
final Object old = clear();
renderable = image;
reset();
if (error != null) {
firePropertyChange("error", error, null);
}
firePropertyChange("image", old, image);
repaint();
}
/**
* Sets the source rendered image. If the given image is larger than the size
* given at construction time, then it will be scaled down when first needed.
* A {@code null} value remove the current image.
*
* @param image The image to display, or {@code null} if none.
*/
public void setImage(RenderedImage image) {
if (image != null) {
final float scale = Math.min(
renderedSize.width / (float) image.getWidth(),
renderedSize.height / (float) image.getHeight());
if (scale < 1) {
final Float sc = Float.valueOf(scale);
final Float tr = 0f; // Seems mandatory, despite what JAI javadoc said.
image = ScaleDescriptor.create(image, sc, sc, tr, tr, null, null);
}
}
final Throwable error = this.error;
final Object old = clear();
rendered = image;
reset();
if (error != null) {
firePropertyChange("error", error, null);
}
firePropertyChange("image", old, image);
repaint();
}
/**
* Removes the current image (if any) and paints the stack trace of the given exception
* instead. This method is invoked when the client code failed to create the image to
* display, typically because of an {@link java.io.IOException}.
* <p>
* The error is cleaned when a {@code setImage(...)} method is invoked.
*
* @param error The error to paint, or {@code null} if none.
*
* @since 3.05
*/
public void setError(final Throwable error) {
final Object old = this.error;
final Object image = clear();
this.error = error;
reset();
if (image != null) {
firePropertyChange("image", image, null);
}
firePropertyChange("error", old, error);
repaint();
}
/**
* Returns the progress pane, creating it if needed.
*/
private ProgressPane getProgressPane() {
ProgressPane progressPane = this.progressPane;
if (progressPane == null) {
this.progressPane = progressPane = new ProgressPane(getLocale());
setLayout(new GridBagLayout());
final GridBagConstraints c = new GridBagConstraints();
c.gridx = c.gridy = 0; c.insets.left = c.insets.right = MARGIN;
c.fill = GridBagConstraints.HORIZONTAL; c.weightx=1;
add(progressPane, c);
validate();
}
return progressPane;
}
/**
* The panel showing the progress.
* This panel paints a translucide background below the progress bar.
*
* @author Martin Desruisseaux (Geomatys)
* @version 3.12
*
* @since 3.07
* @module
*/
private static final class ProgressPane extends JComponent {
/** The progress label. */ final JLabel label;
/** The progress bar. */ final JProgressBar bar;
/** Creates a new panel with a label initialized to a default value from the given locale. */
ProgressPane(final Locale locale) {
setLayout(new GridLayout(2, 1, 0, 3));
label = new JLabel(getDefaultProgressLabel(locale), JLabel.CENTER);
bar = new JProgressBar();
setOpaque(false);
setVisible(false);
add(label);
add(bar);
setBorder(BorderFactory.createCompoundBorder(
BorderFactory.createRaisedBevelBorder(),
BorderFactory.createEmptyBorder(6, 15, 9, 15)));
}
/** Paints a translucent rectangle before to paint the panel. */
@Override protected void paintComponent(final Graphics graphics) {
final Graphics2D gr = (Graphics2D) graphics;
final Paint oldPaint = gr.getPaint();
gr.setColor(new Color(240, 240, 240, 192));
gr.fill(new Rectangle2D.Float(0, 0, getWidth(), getHeight()));
gr.setPaint(oldPaint);
super.paintComponent(graphics);
}
}
/**
* Returns the default progress label localized in the given locale.
*/
static String getDefaultProgressLabel(final Locale locale) {
return Vocabulary.getResources(locale).getString(Vocabulary.Keys.Loading);
}
/**
* Shows or hide the progress bar. This method should be invoked with the value {@code true}
* before to invoke {@link #setProgress(int)}, and invoked again with the value {@code false}
* when the operation is completed. This will not be done automatically.
*
* @param visible {@code true} for showing the progress pane, or {@code false} for hiding it.
*
* @since 3.07
*/
public void setProgressVisible(final boolean visible) {
if (visible) {
getProgressPane().setVisible(true);
} else {
final ProgressPane progressPane = this.progressPane;
if (progressPane != null) {
progressPane.setVisible(false);
}
}
}
/**
* Returns {@code true} if the progress bar is visible, or {@code false} otherwise.
* This method returns the last value given to the {@link #setProgressVisible(boolean)}
* method, or {@code false} if the value has never been set.
*
* @return {@code true} if the progress bar is currently visible.
*
* @since 3.07
*/
public boolean isProgressVisible() {
ProgressPane progressPane = this.progressPane;
return (progressPane != null) && progressPane.isVisible();
}
/**
* Sets the label to display on top of the progress bar. If this method has never been
* invoked, then the default value is {@code "Loading..."} localized for the current locale.
*
* @param label The new label to print on top of the progress bar.
*
* @since 3.07
*/
public void setProgressLabel(final String label) {
getProgressPane().label.setText(label);
}
/**
* Returns the current label to display on top of the progress bar. This is the last value
* given to the {@link #setProgressLabel(String)} method, or {@code "Loading..."} localized
* for the current locale if the value has never been set.
*
* @return The current label to print on top of the progress bar.
*
* @since 3.07
*/
public String getProgressLabel() {
ProgressPane progressPane = this.progressPane;
return (progressPane != null) ? progressPane.label.getText() : getDefaultProgressLabel(getLocale());
}
/**
* Sets the progress done, as a percentage between 0 and 100 inclusive. This method can
* be invoked during lengthly operation like reading the image from a file. The lengthly
* operation is typically run in a background thread, but this method shall be invoked
* from the <cite>Swing</cite> thread only.
* <p>
* The {@link #setProgressVisible(boolean)} method should be invoked before to lengthly
* operation begin, and when it is finished.
*
* @param percentageDone The percentage done as a number between 0 and 100 inclusive.
*
* @since 3.07
*/
public void setProgress(final int percentageDone) {
getProgressPane().bar.setValue(percentageDone);
}
/**
* Returns the current progress, as a percentage between 0 and 100 inclusive. This is the last
* value given to the {@link #setProgress(int)} method, or 0 if the value has never been set.
*
* @return The current progress percentage.
*
* @since 3.07
*/
public int getProgress() {
ProgressPane progressPane = this.progressPane;
return (progressPane != null) ? progressPane.bar.getValue() : 0;
}
/**
* Resets the default zoom. This method overrides the default implementation in
* order to keep the <var>y</var> axis in its Java2D direction (<var>y</var>
* value increasing down), which is the usual direction of most image.
*/
@Override
public void reset() {
reset(getZoomableBounds(null), false);
/*
* If the image is smaller than the widget area, computes an additional transform for
* getting a scale factor of approximatively one. In other words, get a unscaled image.
*/
double scale = Math.min(zoom.getScaleX(), zoom.getScaleY());
if (scale > 1) {
final Rectangle2D area = getArea();
if (area != null) {
scale = 1/scale;
final double cx = area.getCenterX();
final double cy = area.getCenterY();
final AffineTransform change = AffineTransform.getTranslateInstance(cx, cy);
change.scale(scale, scale);
change.translate(-cx, -cy);
transform(change);
}
}
}
/**
* Returns the image bounds, or {@code null} if none. This is used by
* {@link ZoomPane} in order to set the initial zoom.
*/
@Override
public Rectangle2D getArea() {
final RenderedImage rendered = this.rendered; // Protect from change in an other thread
if (rendered != null) {
return ImageUtilities.getBounds(rendered);
}
return null;
}
/**
* Paints the image. If the image was a {@link RenderableImage}, then a {@link RenderedImage}
* will be computed in a background thread when this method is first invoked. If the rendering
* fails, then the exception stack trace will be painted.
*/
@Override
protected void paintComponent(final Graphics2D graphics) {
if (error == null) {
if (rendered == null) {
if (renderable != null && render == null) {
final Render r = new Render();
render = r;
r.execute();
}
// Leave the canvas empty. A repaint event will be posted
// later when the rendered image will be ready.
return;
}
try {
graphics.drawRenderedImage(rendered, zoom);
return;
} catch (RuntimeException e) {
error = e;
// Fallthrough the code below.
}
}
graphics.setColor(getForeground());
Exceptions.paintStackTrace(graphics, getZoomableBounds(null), error);
}
/**
* The worker which will create a {@link RenderedImage} from a {@link RenderableImage}.
*
* @author Martin Desruisseaux (IRD, Geomatys)
* @version 3.05
*
* @since 3.05
* @module
*/
private final class Render extends SwingWorker<RenderedImage,Object> {
/**
* The renderable image, assigned from the Swing thread and used from the background
* thread. We must do this assignment for protecting the value from concurrent change.
*/
private final RenderableImage producer = renderable;
/**
* Creates the rendered image.
*/
@Override
protected RenderedImage doInBackground() {
int width = renderedSize.width;
int height = renderedSize.height;
/*
* Setting one of the dimension to zero instruct createScaledRendering(...)
* to compute it from the other one and the aspect ratio of the image.
*/
if (width < height) {
width = 0;
} else {
height = 0;
}
return producer.createScaledRendering(width, height, null);
}
/**
* Invoked from the Swing thread when the image creation has been completed,
* has been interrupted or failed. This method set the image of error field
* accordingly.
*/
@Override
protected void done() {
if (render == this) {
render = null; // Declare the task as completed.
try {
rendered = get();
} catch (InterruptedException e) {
/*
* The task has been canceled, normally from the ImagePane.clear() method.
* Do not change the state and do not repaint, because the caller of clear()
* is going to set a new image anyway.
*/
return;
} catch (ExecutionException e) {
error = e.getCause();
}
repaint();
}
}
}
}