/*
* 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.awt.Color;
import java.awt.Dimension;
import java.awt.Component;
import java.awt.EventQueue;
import java.io.IOException;
import java.util.Map;
import java.util.HashMap;
import java.util.concurrent.ExecutionException;
import java.text.NumberFormat;
import javax.swing.SwingWorker;
import javax.swing.JProgressBar;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.text.ParseException;
import org.apache.sis.math.Vector;
import org.geotoolkit.math.VectorPair;
import org.apache.sis.math.Statistics;
import org.geotoolkit.gui.swing.Dialog;
import org.geotoolkit.gui.swing.Plot2D;
import org.geotoolkit.internal.swing.SwingUtilities;
import org.geotoolkit.image.io.mosaic.TileManager;
import org.geotoolkit.image.io.mosaic.MosaicProfiler;
import org.geotoolkit.display.axis.NumberGraduation;
import org.geotoolkit.resources.Vocabulary;
import org.apache.sis.util.logging.Logging;
import static org.apache.sis.util.ArgumentChecks.ensurePositive;
/**
* A plot of the estimated efficiency of loading tiles using a given mosaic. Given a {@link TileManager},
* this method computes an estimation of the efficiency of loading tiles at different subsampling levels.
* The details of the calculation is documented in the {@link MosaicProfiler} class.
* <p>
* The calculations are performed by the {@link #plotEfficiency plotEfficiency} method,
* which may require a long execution time. Consequently this method should be invoked from a
* background thread rather than the <cite>Swing</cite> thread. As a convenience, the
* {@link #plotLater plotLater} method is provided for delegating the calculation to a
* background thread.
*
* @author Martin Desruisseaux (Geomatys)
* @version 3.00
*
* @since 3.00
* @module
*/
@SuppressWarnings("serial")
public class MosaicPerformanceGraph extends Plot2D implements Dialog {
/**
* Small tolerance factor for the comparison of floating point numbers.
*/
private static final double EPS = 1E-6;
/**
* The seed to use for the random number generator.
*/
private final long seed;
/**
* If {@code true}, automatically clears the plot before to add the result of a new computation.
*/
private boolean clearBeforePlot = true;
/**
* Number of samplings to performs per subsampling.
*/
private int imagesPerSubsampling = 100;
/**
* The worker which will compute the graph in a background thread, or {@code null} if
* no worker is about to be executed. This field is used in order to allow changes to
* the {@code Delayed} argument before the execution really start.
*/
private transient volatile Worker worker;
/**
* The progress bar to inform of lengthly operation, or {@code null} if none.
*/
private JProgressBar progressBar;
/**
* Creates a default graph.
*/
public MosaicPerformanceGraph() {
super(true, false);
seed = System.currentTimeMillis();
setPreferredSize(new Dimension(600, 400));
}
/**
* Creates a profiler for the given mosaic. This method is invoked automatically
* by {@link #plotEfficiency(String, TileManager)} when a new plot has been
* requested. The default implementation creates a profiler with {@linkplain
* MosaicProfiler#setSubsamplingChangeAllowed subsampling change allowed}.
* Subclasses can overwrite this method for configuring the profiler in a different way.
*
* @param tiles The mosaic to profile.
* @return A profiler for the given mosaic.
* @throws IOException if an I/O operation was required and failed.
*/
protected MosaicProfiler createProfiler(final TileManager tiles) throws IOException {
final MosaicProfiler profiler = new MosaicProfiler(tiles);
profiler.setSubsamplingChangeAllowed(true);
return profiler;
}
/**
* Returns the number of images to be requested for each subsampling level.
*
* @return The current number of image loadings to be done or simulated per subsampling level.
*/
public int getImagesPerSubsampling() {
return imagesPerSubsampling;
}
/**
* Sets the number of images to be requested for each subsampling level.
*
* @param n The new number of image loadings to be done or simulated per subsampling level.
*/
public void setImagesPerSubsampling(final int n) {
ensurePositive("n", n);
final int old = imagesPerSubsampling;
imagesPerSubsampling = n;
firePropertyChange("imagesPerSubsampling", old, n);
}
/**
* Returns the progress bar given to the last to {@link #setProgressBar(JProgressBar)}.
* The default value is {@code null}, i.e. this widget do not provides any progress bar
* by itself.
*
* @return The progress bar given to the last to {@code setProgressBar}.
*/
public JProgressBar getProgressBar() {
return progressBar;
}
/**
* Sets the progress bar to inform of lengthly operation, or {@code null} if none.
* The given progress bar is expected to accept a range of values from 0 to 100 inclusive.
* If the progress bar accepts a wider range of values, only the [0 … 100] range
* will be used.
* <p>
* The given progress bar is not displayed in this widget. It is caller
* responsibility to provide a progress bar visible in his own widget.
*
* @param bar The progress bar, or {@code null} if none.
*/
public void setProgressBar(final JProgressBar bar) {
final JProgressBar old = progressBar;
progressBar = bar;
firePropertyChange("progressBar", old, bar);
}
/**
* If {@code true}, any call to {@code plotEfficiency} will clear the previous plot
* before to add the result of a new calculation. If {@code false}, then the result of
* {@code plotEfficiency} will be a new series added to the existing ones.
* <p>
* The default value is {@code true}.
*
* @return Whatever the result of {@code plotEfficiency} will replace any previous plot.
*/
public boolean getClearBeforePlot() {
return clearBeforePlot;
}
/**
* Sets whatever the result of {@code plotEfficiency} should replace any previous plot.
*
* @param clear Whatever the result of {@code plotEfficiency} should replace any previous plot.
*/
public void setClearBeforePlot(final boolean clear) {
final boolean old = clearBeforePlot;
clearBeforePlot = clear;
firePropertyChange("clearBeforePlot", old, clear);
}
/**
* Replaces NaN values by the previous value in the given array.
*/
private static void replaceNaN(final float[] array) {
float last = Float.NaN;
for (int i=0; i<array.length; i++) {
final float value = array[i];
if (Float.isNaN(value)) {
array[i] = last;
} else {
last = value;
}
}
}
/**
* Adds a plot calculated for the given mosaic. This method will run the profiler for every
* uniform subsampling values ranging from the {@linkplain MosaicProfiler#getMinSubsampling
* minimum} to the {@linkplain MosaicProfiler#getMaxSubsampling maximum} subsampling value,
* inclusives.
* <p>
* This method can be invoked from any thread - it doesn't need to be the <cite>Swing</cite>
* one. Since this method may take a while, it is recommended to invoke it from a background
* thread.
*
* @param name The name to given to the plot, or {@code null} if none.
* @param tiles The mosaic for which to plot the estimated cost of loading images.
* @throws IOException if an I/O operation was required and failed.
*/
public final void plotEfficiency(final String name, final TileManager tiles) throws IOException {
/*
* We do not allow the user to override this method (it is final) because it is hard to
* make plotLater to invoke it, so the user could be confused to see his implementation
* ignored despite what our javadoc said. PlotLater needs to invoke the private method
* below with an explicit Worker argument. We can't take the value of this.worker field
* because it could have changed during the window of vulnerability between assignation
* of this.worker and execution of plotEfficiency. Furthermore we don't want to be
* confused if a worker and someone else invoke plotEfficiency in same time.
*/
plotEfficiency(name, tiles, null);
}
/**
* Implementation of {@code plotEfficiency} callable from a worker. This method is
* not public because we don't want to expose the worker in public API.
*
* @param name The name to given to the plot, or {@code null} if none.
* @param tiles The mosaic for which to plot the estimated cost of loading images.
* @param worker The worker that invoked this method, or {@code null} if none.
* @return The mosaic which has been profiled, or {@code null} if the operation has
* been canceled before completion.
* @throws IOException if an I/O operation was required and failed.
*/
private TileManager plotEfficiency(final String name, final TileManager tiles, final Worker worker)
throws IOException
{
final MosaicProfiler profiler = createProfiler(tiles);
final Dimension minSubsampling = profiler.getMinSubsampling();
final Dimension maxSubsampling = profiler.getMaxSubsampling();
final int ms = Math.max(minSubsampling.width, minSubsampling.height);
final int ns = Math.min(maxSubsampling.width, maxSubsampling.height) - ms + 1;
profiler.setMaxSubsampling(ms);
final float[] cost = new float[ns];
final float[] high = new float[ns];
final float[] low = new float[ns];
for (int i=0; i<ns; i++) {
if (worker != null) {
if (worker.isCancelled()) {
return null;
}
worker.progress(i*100 / ns);
}
profiler.setSeed(seed); // Use the same random values for each subsamplings.
profiler.setMinSubsampling(i+ms);
final Statistics stats = profiler.estimateEfficiency(imagesPerSubsampling);
final double c = stats.mean();
final double stdv = stats.standardDeviation(false);
cost[i] = (float) c;
low [i] = (float) Math.max(c - stdv, stats.minimum());
high[i] = (float) Math.min(c + stdv, stats.maximum());
}
// Reset the profiler to its initial state.
profiler.setMinSubsampling(minSubsampling);
profiler.setMaxSubsampling(maxSubsampling);
// Workaroud the points calculated with only 1 value.
replaceNaN(low);
replaceNaN(high);
/*
* Computes the main line, which a "stepwise" visual effect
* for emphasing the discontinuous nature of subsampling.
* Computes also the standard deviation to paint around the main line.
*/
final Vector x = Vector.createSequence(ms, 1, 1+ns);
final Vector xm = Vector.createSequence(ms-0.5, 1, 1+ns);
final VectorPair upper = new VectorPair(x, Vector.create(high, false));
final VectorPair main = new VectorPair(xm, Vector.create(cost, false));
final VectorPair lower = new VectorPair(x, Vector.create(low, false));
upper.makeStepwise(+2);
main .makeStepwise( 0);
lower.makeStepwise(-2);
upper.omitColinearPoints(EPS, EPS);
main .omitColinearPoints(EPS, EPS);
lower.omitColinearPoints(EPS, EPS);
final Vector xs = upper.getX().concatenate(lower.getX().reverse());
final Vector ys = upper.getY().concatenate(lower.getY().reverse());
EventQueue.invokeLater(new Runnable() {
@Override public void run() {
if (getClearBeforePlot()) {
clear();
}
final int ns = getSeries().size();
final Color color = DEFAULT_COLORS.get((ns/2) % DEFAULT_COLORS.size());
final Color trans = new Color(color.getRGB() & 0x20FFFFFF, true);
final Map<String,Object> properties = new HashMap<>(4);
properties.put("Name", name);
properties.put("Paint", trans);
properties.put("Fill", Boolean.TRUE);
if (ns == 0) {
final Vocabulary resources = Vocabulary.getResources(getLocale());
addXAxis(resources.getString(Vocabulary.Keys.Subsampling));
addYAxis(resources.getString(Vocabulary.Keys.Efficiency));
}
final Series series = addSeries(properties, xs, ys);
properties.remove("Fill");
properties.put("Paint", color);
addSeries(properties, main.getX(), main.getY());
if (ns == 0) {
final NumberGraduation grad = (NumberGraduation) getAxes(series)[1].getGraduation();
grad.setFormat(NumberFormat.getPercentInstance(grad.getLocale()));
}
}
});
return profiler.mosaic;
}
/**
* Specifies a mosaic to be profiled in a background thread. An instance of this interface
* can be given to the {@link MosaicPerformanceGraph#plotLater(String, Delayed, long)} method.
* The methods defined in this interface will be called in a background thread some time after
* {@code plotLater}. The workflow is as below:
*
* <ol>
* <li><p>An instance of {@code Delayed} is given to the {@code plotLater} method.</p></li>
* <li><p>A background thread will sleep the specified amount of time. If {@code plotLater}
* is invoked again while the background thread is sleeping, then the old {@code Delayed}
* instance is discarded and replaced by the new one.</p></li>
* <li><p>After the above delay, the {@link #getTileManager()} method is
* invoked in the background thread. The returned mosaic is given to
* {@link MosaicPerformanceGraph#plotEfficiency(String, TileManager)}.</p></li>
* <li><p>After the {@code plotEfficiency} method terminated, exactly one of the
* following methods is invoked in the <cite>Swing</cite> thread:
* <ul>
* <li>{@link #done(TileManager)} on success.</li>
* <li>{@link #failed(Throwable)} on failure.</li>
* </ul></p></li>
* </ol>
*
* @author Martin Desruisseaux (Geomatys)
* @version 3.00
*
* @since 3.00
* @module
*/
public interface Delayed {
/**
* Returns the mosaic to profile. This method is invoked in a background
* thread before the profiling begin.
*
* @return The mosaic as a tile manager.
* @throws IOException if an I/O operation was required and failed.
*/
TileManager getTileManager() throws IOException;
/**
* Invoked on the <cite>Swing</cite> thread after the profiling has been completed.
* This is usually the instance returned by {@link #getTileManager()}.
*
* @param mosaic The mosaic which has been profiled.
*/
void done(TileManager mosaic);
/**
* Invoked on the <cite>Swing</cite> thread if an exception occurred during the profiling.
*
* @param exception The exception which occurred.
*/
void failed(Throwable exception);
}
/**
* Adds a plot for the mosaic created by a given builder. This method can be invoked from any
* thread and returns immediately. The actual plot calculation is delegated to a background
* thread.
*
* @param name The name to given to the plot, or {@code null} if none.
* @param delayed Provides the {@link TileManager} and the methods to callback on success or failure.
* @param delay How long to wait (in milliseconds) before to perform the calculation.
*/
public void plotLater(final String name, final Delayed delayed, final long delay) {
EventQueue.invokeLater(new Runnable() {
@Override public void run() {
Worker worker = MosaicPerformanceGraph.this.worker;
if (worker == null || !worker.schedule(name, delayed, delay)) {
worker = new Worker();
if (!worker.schedule(name, delayed, delay)) {
throw new AssertionError();
}
worker.execute();
}
MosaicPerformanceGraph.this.worker = worker;
final JProgressBar progress = getProgressBar();
if (progress != null) {
progress.setEnabled(true);
progress.setIndeterminate(true);
}
}
});
}
/**
* The worker which will computes the graph in background. An instance of this class will
* be created everytime needed, but will wait an arbitrary amount of time before to begin
* its job in case the builder configuration change.
*
* @author Martin Desruisseaux (Geomatys)
* @version 3.00
*
* @since 3.00
* @module
*/
private final class Worker extends SwingWorker<TileManager,Object> implements PropertyChangeListener {
/**
* The name of the plot to add.
*/
private String name;
/**
* Provides the mosaic builder for which to plot the performance graph.
*/
private Delayed delayed;
/**
* When to starts computation, in milliseconds since January 1st, 1970.
*/
private long time;
/**
* {@code true} if this worker started its job.
*/
private boolean running;
/**
* Creates a new worker.
*/
Worker() {
addPropertyChangeListener(this);
}
/**
* Schedule a plot for the given builder. Returns {@code true} on success, or
* {@code false} if an other instance of {@code Worker} needs to be created.
*/
synchronized boolean schedule(final String name, final Delayed delayed, final long delay) {
if (running) {
cancel(false);
return false;
}
this.name = name;
this.delayed = delayed;
this.time = delay + System.currentTimeMillis();
return true;
}
/**
* Waits for the delay, then plots the graph.
*/
@Override
protected TileManager doInBackground() throws IOException {
final String name;
final Delayed delayed;
synchronized (this) {
long delay;
while ((delay = time - System.currentTimeMillis()) > 1) {
try {
wait(delay);
} catch (InterruptedException ex) {
// Ignore and go back to work.
}
}
name = this.name;
delayed = this.delayed;
running = true;
}
return plotEfficiency(name, delayed.getTileManager(), this);
}
/**
* Invoked by {@code plotEfficiency} for setting the progress. Defined here because
* {@code setProgress} has protected access.
*
* @param p The progress as a value in the [0 ... 100] range.
*/
public void progress(final int p) {
if (!isDone()) {
setProgress(p);
}
}
/**
* Invoked from the event dispatch thread after a call to {@link #progress}.
* Performs the actual update of the progress bar.
*/
@Override
public void propertyChange(final PropertyChangeEvent event) {
if (event.getPropertyName().equals("progress")) {
final JProgressBar progress = getProgressBar();
if (progress != null) {
progress.setIndeterminate(false);
progress.setValue(getProgress());
}
}
}
/**
* Executed from the event dispatch thread. Discards the reference to the worker if it
* was this instance. This is needed for letting {@link MosaicPerformanceGraph#plotLater}
* know that it can set the state of the progress bar when a new plot is requested.
*/
@Override
protected void done() {
if (worker == this) {
worker = null;
if (!isCancelled()) {
/*
* Note: get() below should not return null even if plotEfficiency(...)
* returned null, because the later call should occur only when the plot has
* been cancelled and we checked in the above line that this is not the case.
*/
try {
delayed.done(get());
} catch (ExecutionException e) {
delayed.failed(e.getCause());
} catch (InterruptedException e) {
// Should never happen, since the task is completed.
Logging.unexpectedException(null, MosaicPerformanceGraph.class, "plotEfficiency", e);
}
final JProgressBar progress = getProgressBar();
if (progress != null) {
progress.setIndeterminate(false);
progress.setValue(100);
progress.setEnabled(false);
}
}
}
}
}
/**
* Forces the current values to be taken from the editable fields and set them as the
* current values. The default implementation does nothing since there is no editable
* fields in this widget.
*
* @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);
}
}