/*
* Copyright 2010-2015 Institut Pasteur.
*
* This file is part of Icy.
*
* Icy is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Icy 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Icy. If not, see <http://www.gnu.org/licenses/>.
*/
package icy.gui.component.math;
import icy.gui.component.BorderedPanel;
import icy.math.ArrayMath;
import icy.math.Histogram;
import icy.math.MathUtil;
import icy.type.collection.array.Array1DUtil;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.util.EventListener;
import javax.swing.BorderFactory;
/**
* Histogram component.
*
* @author Stephane
*/
public class HistogramPanel extends BorderedPanel
{
public static interface HistogramPanelListener extends EventListener
{
/**
* histogram need to be refreshed (send values for recalculation)
*/
public void histogramNeedRefresh(HistogramPanel source);
}
/**
*
*/
private static final long serialVersionUID = -3932807727576675217L;
protected static final int BORDER_WIDTH = 2;
protected static final int BORDER_HEIGHT = 2;
protected static final int MIN_SIZE = 16;
/**
* internal histogram
*/
Histogram histogram;
/**
* histogram data cache
*/
private double[] histogramData;
/**
* histogram properties
*/
double minValue;
double maxValue;
boolean integer;
/**
* display properties
*/
boolean logScaling;
boolean useLAFColors;
Color color;
Color backgroundColor;
/**
* internals
*/
boolean updating;
/**
* Create a new histogram panel for the specified value range.<br>
* By default it uses a Logarithm representation (modifiable via {@link #setLogScaling(boolean)}
*
* @param minValue
* @param maxValue
* @param integer
*/
public HistogramPanel(double minValue, double maxValue, boolean integer)
{
super();
setBorder(BorderFactory.createEmptyBorder(BORDER_HEIGHT, BORDER_WIDTH, BORDER_HEIGHT, BORDER_WIDTH));
setMinimumSize(new Dimension(100, 100));
setMaximumSize(new Dimension(Short.MAX_VALUE, Short.MAX_VALUE));
histogram = new Histogram(0d, 1d, 1, true);
histogramData = new double[0];
this.minValue = minValue;
this.maxValue = maxValue;
this.integer = integer;
logScaling = true;
useLAFColors = true;
// default drawing color
color = Color.white;
backgroundColor = Color.darkGray;
buildHistogram(minValue, maxValue, integer);
updating = false;
}
/**
* Returns true when histogram is being calculated.
*/
public boolean isUpdating()
{
return updating;
}
/**
* Call this method to inform you start histogram computation (allow the panel to display
* "computing" message).</br>
* You need to call {@link #done()} when computation is done.
*
* @see #done()
*/
public void reset()
{
histogram.reset();
// start histogram calculation
updating = true;
}
/**
* @deprecated Use <code>getHistogram.addValue(double)</code> instead.
*/
@Deprecated
public void addValue(double value)
{
histogram.addValue(value);
}
/**
* @deprecated Use <code>getHistogram.addValue(Object, boolean signed)</code> instead.
*/
@Deprecated
public void addValues(Object array, boolean signed)
{
histogram.addValues(array, signed);
}
/**
* @deprecated Use <code>getHistogram.addValue(byte[])</code> instead.
*/
@Deprecated
public void addValues(byte[] array, boolean signed)
{
histogram.addValues(array, signed);
}
/**
* @deprecated Use <code>getHistogram.addValue(short[])</code> instead.
*/
@Deprecated
public void addValues(short[] array, boolean signed)
{
histogram.addValues(array, signed);
}
/**
* @deprecated Use <code>getHistogram.addValue(int[])</code> instead.
*/
@Deprecated
public void addValues(int[] array, boolean signed)
{
histogram.addValues(array, signed);
}
/**
* @deprecated Use <code>getHistogram.addValue(long[])</code> instead.
*/
@Deprecated
public void addValues(long[] array, boolean signed)
{
histogram.addValues(array, signed);
}
/**
* @deprecated Use <code>getHistogram.addValue(float[])</code> instead.
*/
@Deprecated
public void addValues(float[] array)
{
histogram.addValues(array);
}
/**
* @deprecated Use <code>getHistogram.addValue(double[])</code> instead.
*/
@Deprecated
public void addValues(double[] array)
{
histogram.addValues(array);
}
/**
* Returns the adjusted size (linear / log normalized) of the specified bin.
*
* @see #getBinSize(int)
*/
public double getAdjustedBinSize(int index)
{
// cache
final double[] data = histogramData;
if ((index >= 0) && (index < data.length))
return data[index];
return 0d;
}
/**
* Returns the size of the specified bin (number of element in the bin)
*
* @see icy.math.Histogram#getBinSize(int)
*/
public int getBinSize(int index)
{
return histogram.getBinSize(index);
}
/**
* @see icy.math.Histogram#getBinNumber()
*/
public int getBinNumber()
{
return histogram.getBinNumber();
}
/**
* @see icy.math.Histogram#getBinWidth()
*/
public double getBinWidth()
{
return histogram.getBinWidth();
}
/**
* @see icy.math.Histogram#getBins()
*/
public int[] getBins()
{
return histogram.getBins();
}
/**
* Invoke this method when the histogram calculation has been completed to refresh data cache.
*/
public void done()
{
refreshDataCache();
// end histogram calculation
updating = false;
}
/**
* Returns the minimum allowed value of the histogram.
*/
public double getMinValue()
{
return histogram.getMinValue();
}
/**
* Returns the maximum allowed value of the histogram.
*/
public double getMaxValue()
{
return histogram.getMaxValue();
}
/**
* Returns true if the input value are integer values only.<br>
* This is used to adapt the bin number of histogram..
*/
public boolean isIntegerType()
{
return histogram.isIntegerType();
}
/**
* Returns true if histogram is displayed with LOG scaling
*/
public boolean getLogScaling()
{
return logScaling;
}
/**
* Returns true if histogram use LAF color scheme.
*
* @see #getColor()
* @see #getBackgroundColor()
*/
public boolean getUseLAFColors()
{
return useLAFColors;
}
/**
* Returns the drawing color
*/
public Color getColor()
{
return color;
}
/**
* Returns the background color
*/
public Color getBackgroundColor()
{
return color;
}
/**
* Get histogram object
*/
public Histogram getHistogram()
{
return histogram;
}
/**
* Get computed histogram data
*/
public double[] getHistogramData()
{
return histogramData;
}
/**
* Set minimum, maximum and integer values at once
*/
public void setMinMaxIntValues(double min, double max, boolean intType)
{
// test with cached value first
if ((minValue != min) || (maxValue != max) || (integer != intType))
buildHistogram(min, max, intType);
// then test with uncached value (histo being updated)
else if ((histogram.getMinValue() != min) || (histogram.getMaxValue() != max)
|| (histogram.isIntegerType() != intType))
buildHistogram(min, max, intType);
}
/**
* Set to true to display histogram with LOG scaling (else it uses linear scaling).
*/
public void setLogScaling(boolean value)
{
if (logScaling != value)
{
logScaling = value;
refreshDataCache();
}
}
/**
* Set to true to use LAF color scheme.
*
* @see #setColor(Color)
* @see #setBackgroundColor(Color)
*/
public void setUseLAFColors(boolean value)
{
if (useLAFColors != value)
{
useLAFColors = value;
repaint();
}
}
/**
* Set the drawing color
*/
public void setColor(Color value)
{
if (!color.equals(value))
{
color = value;
if (!useLAFColors)
repaint();
}
}
/**
* Set the background color
*/
public void setBackgroundColor(Color value)
{
if (!backgroundColor.equals(value))
{
backgroundColor = value;
if (!useLAFColors)
repaint();
}
}
protected void checkHisto()
{
// create temporary histogram
final Histogram newHisto = new Histogram(histogram.getMinValue(), histogram.getMaxValue(), Math.max(
getClientWidth(), MIN_SIZE), histogram.isIntegerType());
// histogram properties changed ?
if (!hasSameProperties(newHisto))
{
// set new histogram
histogram = newHisto;
// notify listeners so they can fill it
fireHistogramNeedRefresh();
}
}
protected void buildHistogram(double min, double max, boolean intType)
{
// create temporary histogram
final Histogram newHisto = new Histogram(min, max, Math.max(getClientWidth(), MIN_SIZE), intType);
// histogram properties changed ?
if (!hasSameProperties(newHisto))
{
// set new histogram
histogram = newHisto;
// notify listeners so they can fill it
fireHistogramNeedRefresh();
}
}
/**
* Return true if specified histogram has same bounds and number of bin than current one
*/
protected boolean hasSameProperties(Histogram h)
{
return (histogram.getBinNumber() == h.getBinNumber()) && (histogram.getMinValue() == h.getMinValue())
&& (histogram.getMaxValue() == h.getMaxValue()) && (histogram.isIntegerType() == h.isIntegerType());
}
/**
* update histogram data cache
*/
protected void refreshDataCache()
{
// get histogram data
final double[] newHistogramData = Array1DUtil.intArrayToDoubleArray(histogram.getBins(), false);
// we want all values to >= 1
final double min = ArrayMath.min(newHistogramData);
MathUtil.add(newHistogramData, min + 1f);
// log
if (logScaling)
MathUtil.log(newHistogramData);
// normalize data
MathUtil.normalize(newHistogramData);
// get new data cache and apply min, max, integer type
histogramData = newHistogramData;
minValue = getMinValue();
maxValue = getMaxValue();
integer = isIntegerType();
// request repaint
repaint();
}
/**
* Returns the ratio to convert a data value to corresponding pixel X position
*/
protected double getDataToPixelRatio()
{
final double pixelRange = Math.max(getClientWidth() - 1, 32);
final double dataRange = maxValue - minValue;
if (dataRange != 0d)
return pixelRange / dataRange;
return 0d;
}
/**
* Returns the ratio to convert a pixel X position to corresponding data value
*/
protected double getPixelToDataRatio()
{
final double pixelRange = Math.max(getClientWidth() - 1, 32);
final double dataRange = maxValue - minValue;
if (pixelRange != 0d)
return dataRange / pixelRange;
return 0d;
}
/**
* Returns the ratio to convert a pixel X position to corresponding histo bin
*/
protected double getPixelToHistoRatio()
{
final double histogramRange = histogramData.length - 1;
final double pixelRange = Math.max(getClientWidth() - 1, 32);
if (pixelRange != 0d)
return histogramRange / pixelRange;
return 0d;
}
/**
* Convert a data value to the corresponding pixel position
*/
public int dataToPixel(double value)
{
return (int) Math.round(((value - minValue) * getDataToPixelRatio())) + getClientX();
}
/**
* Convert a pixel position to corresponding data value
*/
public double pixelToData(int value)
{
final double data = ((value - getClientX()) * getPixelToDataRatio()) + minValue;
return Math.min(Math.max(data, minValue), maxValue);
}
/**
* Convert a pixel position to corresponding bin index
*/
public int pixelToBin(int value)
{
final int index = (int) Math.round((value - getClientX()) * getPixelToHistoRatio());
return Math.min(Math.max(index, 0), histogramData.length - 1);
}
/**
* Notify all listeners that histogram need to be recomputed
*/
protected void fireHistogramNeedRefresh()
{
for (HistogramPanelListener l : listenerList.getListeners(HistogramPanelListener.class))
l.histogramNeedRefresh(this);
}
public void addListener(HistogramPanelListener l)
{
listenerList.add(HistogramPanelListener.class, l);
}
public void removeListener(HistogramPanelListener l)
{
listenerList.remove(HistogramPanelListener.class, l);
}
@Override
protected void paintComponent(Graphics g)
{
final Color fc;
final Color bc;
if (useLAFColors)
{
fc = getForeground();
bc = getBackground();
}
else
{
fc = color;
bc = backgroundColor;
}
final Graphics2D g2 = (Graphics2D) g.create();
g2.setColor(fc);
g2.setBackground(bc);
// background color
if (isOpaque())
g2.clearRect(0, 0, getWidth(), getHeight());
// data cache
final double ratio = getPixelToHistoRatio();
final double[] data = histogramData;
// not yet computed
if (data.length != 0)
{
final int histoRange = data.length - 1;
final int hRange = getClientHeight() - 1;
final int bottom = getClientY() + hRange;
final int l = getClientX();
final int r = l + getClientWidth();
for (int i = l; i < r; i++)
{
int index = (int) Math.round((i - l) * ratio);
if (index < 0)
index = 0;
else if (index > histoRange)
index = histoRange;
g2.drawLine(i, bottom, i, bottom - (int) Math.round(data[index] * hRange));
}
}
if ((data.length == 0) || updating)
{
final int x = (getWidth() / 2) - 60;
final int y = (getHeight() / 2) - 20;
g2.drawString("computing...", x, y);
}
g2.dispose();
// just check for histogram properties change
checkHisto();
}
}