001/*
002 * Copyright 2010-2015 Institut Pasteur.
003 * 
004 * This file is part of Icy.
005 * 
006 * Icy is free software: you can redistribute it and/or modify
007 * it under the terms of the GNU General Public License as published by
008 * the Free Software Foundation, either version 3 of the License, or
009 * (at your option) any later version.
010 * 
011 * Icy is distributed in the hope that it will be useful,
012 * but WITHOUT ANY WARRANTY; without even the implied warranty of
013 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
014 * GNU General Public License for more details.
015 * 
016 * You should have received a copy of the GNU General Public License
017 * along with Icy. If not, see <http://www.gnu.org/licenses/>.
018 */
019package icy.gui.component.math;
020
021import icy.gui.component.BorderedPanel;
022import icy.math.ArrayMath;
023import icy.math.Histogram;
024import icy.math.MathUtil;
025import icy.type.collection.array.Array1DUtil;
026
027import java.awt.Color;
028import java.awt.Dimension;
029import java.awt.Graphics;
030import java.awt.Graphics2D;
031import java.util.EventListener;
032
033import javax.swing.BorderFactory;
034
035/**
036 * Histogram component.
037 * 
038 * @author Stephane
039 */
040public class HistogramPanel extends BorderedPanel
041{
042    public static interface HistogramPanelListener extends EventListener
043    {
044        /**
045         * histogram need to be refreshed (send values for recalculation)
046         */
047        public void histogramNeedRefresh(HistogramPanel source);
048    }
049
050    /**
051     * 
052     */
053    private static final long serialVersionUID = -3932807727576675217L;
054
055    protected static final int BORDER_WIDTH = 2;
056    protected static final int BORDER_HEIGHT = 2;
057    protected static final int MIN_SIZE = 16;
058
059    /**
060     * internal histogram
061     */
062    Histogram histogram;
063    /**
064     * histogram data cache
065     */
066    private double[] histogramData;
067
068    /**
069     * histogram properties
070     */
071    double minValue;
072    double maxValue;
073    boolean integer;
074
075    /**
076     * display properties
077     */
078    boolean logScaling;
079    boolean useLAFColors;
080    Color color;
081    Color backgroundColor;
082
083    /**
084     * internals
085     */
086    boolean updating;
087
088    /**
089     * Create a new histogram panel for the specified value range.<br>
090     * By default it uses a Logarithm representation (modifiable via {@link #setLogScaling(boolean)}
091     * 
092     * @param minValue
093     * @param maxValue
094     * @param integer
095     */
096    public HistogramPanel(double minValue, double maxValue, boolean integer)
097    {
098        super();
099
100        setBorder(BorderFactory.createEmptyBorder(BORDER_HEIGHT, BORDER_WIDTH, BORDER_HEIGHT, BORDER_WIDTH));
101
102        setMinimumSize(new Dimension(100, 100));
103        setMaximumSize(new Dimension(Short.MAX_VALUE, Short.MAX_VALUE));
104
105        histogram = new Histogram(0d, 1d, 1, true);
106        histogramData = new double[0];
107
108        this.minValue = minValue;
109        this.maxValue = maxValue;
110        this.integer = integer;
111
112        logScaling = true;
113        useLAFColors = true;
114        // default drawing color
115        color = Color.white;
116        backgroundColor = Color.darkGray;
117
118        buildHistogram(minValue, maxValue, integer);
119
120        updating = false;
121    }
122
123    /**
124     * Returns true when histogram is being calculated.
125     */
126    public boolean isUpdating()
127    {
128        return updating;
129    }
130
131    /**
132     * Call this method to inform you start histogram computation (allow the panel to display
133     * "computing" message).</br>
134     * You need to call {@link #done()} when computation is done.
135     * 
136     * @see #done()
137     */
138    public void reset()
139    {
140        histogram.reset();
141        // start histogram calculation
142        updating = true;
143    }
144
145    /**
146     * @deprecated Use <code>getHistogram.addValue(double)</code> instead.
147     */
148    @Deprecated
149    public void addValue(double value)
150    {
151        histogram.addValue(value);
152    }
153
154    /**
155     * @deprecated Use <code>getHistogram.addValue(Object, boolean signed)</code> instead.
156     */
157    @Deprecated
158    public void addValues(Object array, boolean signed)
159    {
160        histogram.addValues(array, signed);
161    }
162
163    /**
164     * @deprecated Use <code>getHistogram.addValue(byte[])</code> instead.
165     */
166    @Deprecated
167    public void addValues(byte[] array, boolean signed)
168    {
169        histogram.addValues(array, signed);
170    }
171
172    /**
173     * @deprecated Use <code>getHistogram.addValue(short[])</code> instead.
174     */
175    @Deprecated
176    public void addValues(short[] array, boolean signed)
177    {
178        histogram.addValues(array, signed);
179    }
180
181    /**
182     * @deprecated Use <code>getHistogram.addValue(int[])</code> instead.
183     */
184    @Deprecated
185    public void addValues(int[] array, boolean signed)
186    {
187        histogram.addValues(array, signed);
188    }
189
190    /**
191     * @deprecated Use <code>getHistogram.addValue(long[])</code> instead.
192     */
193    @Deprecated
194    public void addValues(long[] array, boolean signed)
195    {
196        histogram.addValues(array, signed);
197    }
198
199    /**
200     * @deprecated Use <code>getHistogram.addValue(float[])</code> instead.
201     */
202    @Deprecated
203    public void addValues(float[] array)
204    {
205        histogram.addValues(array);
206    }
207
208    /**
209     * @deprecated Use <code>getHistogram.addValue(double[])</code> instead.
210     */
211    @Deprecated
212    public void addValues(double[] array)
213    {
214        histogram.addValues(array);
215    }
216
217    /**
218     * Returns the adjusted size (linear / log normalized) of the specified bin.
219     * 
220     * @see #getBinSize(int)
221     */
222    public double getAdjustedBinSize(int index)
223    {
224        // cache
225        final double[] data = histogramData;
226
227        if ((index >= 0) && (index < data.length))
228            return data[index];
229
230        return 0d;
231    }
232
233    /**
234     * Returns the size of the specified bin (number of element in the bin)
235     * 
236     * @see icy.math.Histogram#getBinSize(int)
237     */
238    public int getBinSize(int index)
239    {
240        return histogram.getBinSize(index);
241    }
242
243    /**
244     * @see icy.math.Histogram#getBinNumber()
245     */
246    public int getBinNumber()
247    {
248        return histogram.getBinNumber();
249    }
250
251    /**
252     * @see icy.math.Histogram#getBinWidth()
253     */
254    public double getBinWidth()
255    {
256        return histogram.getBinWidth();
257    }
258
259    /**
260     * @see icy.math.Histogram#getBins()
261     */
262    public int[] getBins()
263    {
264        return histogram.getBins();
265    }
266
267    /**
268     * Invoke this method when the histogram calculation has been completed to refresh data cache.
269     */
270    public void done()
271    {
272        refreshDataCache();
273
274        // end histogram calculation
275        updating = false;
276    }
277
278    /**
279     * Returns the minimum allowed value of the histogram.
280     */
281    public double getMinValue()
282    {
283        return histogram.getMinValue();
284    }
285
286    /**
287     * Returns the maximum allowed value of the histogram.
288     */
289    public double getMaxValue()
290    {
291        return histogram.getMaxValue();
292    }
293
294    /**
295     * Returns true if the input value are integer values only.<br>
296     * This is used to adapt the bin number of histogram..
297     */
298    public boolean isIntegerType()
299    {
300        return histogram.isIntegerType();
301    }
302
303    /**
304     * Returns true if histogram is displayed with LOG scaling
305     */
306    public boolean getLogScaling()
307    {
308        return logScaling;
309    }
310
311    /**
312     * Returns true if histogram use LAF color scheme.
313     * 
314     * @see #getColor()
315     * @see #getBackgroundColor()
316     */
317    public boolean getUseLAFColors()
318    {
319        return useLAFColors;
320    }
321
322    /**
323     * Returns the drawing color
324     */
325    public Color getColor()
326    {
327        return color;
328    }
329
330    /**
331     * Returns the background color
332     */
333    public Color getBackgroundColor()
334    {
335        return color;
336    }
337
338    /**
339     * Get histogram object
340     */
341    public Histogram getHistogram()
342    {
343        return histogram;
344    }
345
346    /**
347     * Get computed histogram data
348     */
349    public double[] getHistogramData()
350    {
351        return histogramData;
352    }
353
354    /**
355     * Set minimum, maximum and integer values at once
356     */
357    public void setMinMaxIntValues(double min, double max, boolean intType)
358    {
359        // test with cached value first
360        if ((minValue != min) || (maxValue != max) || (integer != intType))
361            buildHistogram(min, max, intType);
362        // then test with uncached value (histo being updated)
363        else if ((histogram.getMinValue() != min) || (histogram.getMaxValue() != max)
364                || (histogram.isIntegerType() != intType))
365            buildHistogram(min, max, intType);
366    }
367
368    /**
369     * Set to true to display histogram with LOG scaling (else it uses linear scaling).
370     */
371    public void setLogScaling(boolean value)
372    {
373        if (logScaling != value)
374        {
375            logScaling = value;
376            refreshDataCache();
377        }
378    }
379
380    /**
381     * Set to true to use LAF color scheme.
382     * 
383     * @see #setColor(Color)
384     * @see #setBackgroundColor(Color)
385     */
386    public void setUseLAFColors(boolean value)
387    {
388        if (useLAFColors != value)
389        {
390            useLAFColors = value;
391            repaint();
392        }
393    }
394
395    /**
396     * Set the drawing color
397     */
398    public void setColor(Color value)
399    {
400        if (!color.equals(value))
401        {
402            color = value;
403            if (!useLAFColors)
404                repaint();
405        }
406    }
407
408    /**
409     * Set the background color
410     */
411    public void setBackgroundColor(Color value)
412    {
413        if (!backgroundColor.equals(value))
414        {
415            backgroundColor = value;
416            if (!useLAFColors)
417                repaint();
418        }
419    }
420
421    protected void checkHisto()
422    {
423        // create temporary histogram
424        final Histogram newHisto = new Histogram(histogram.getMinValue(), histogram.getMaxValue(), Math.max(
425                getClientWidth(), MIN_SIZE), histogram.isIntegerType());
426
427        // histogram properties changed ?
428        if (!hasSameProperties(newHisto))
429        {
430            // set new histogram
431            histogram = newHisto;
432            // notify listeners so they can fill it
433            fireHistogramNeedRefresh();
434        }
435    }
436
437    protected void buildHistogram(double min, double max, boolean intType)
438    {
439        // create temporary histogram
440        final Histogram newHisto = new Histogram(min, max, Math.max(getClientWidth(), MIN_SIZE), intType);
441
442        // histogram properties changed ?
443        if (!hasSameProperties(newHisto))
444        {
445            // set new histogram
446            histogram = newHisto;
447            // notify listeners so they can fill it
448            fireHistogramNeedRefresh();
449        }
450    }
451
452    /**
453     * Return true if specified histogram has same bounds and number of bin than current one
454     */
455    protected boolean hasSameProperties(Histogram h)
456    {
457        return (histogram.getBinNumber() == h.getBinNumber()) && (histogram.getMinValue() == h.getMinValue())
458                && (histogram.getMaxValue() == h.getMaxValue()) && (histogram.isIntegerType() == h.isIntegerType());
459    }
460
461    /**
462     * update histogram data cache
463     */
464    protected void refreshDataCache()
465    {
466        // get histogram data
467        final double[] newHistogramData = Array1DUtil.intArrayToDoubleArray(histogram.getBins(), false);
468
469        // we want all values to >= 1
470        final double min = ArrayMath.min(newHistogramData);
471        MathUtil.add(newHistogramData, min + 1f);
472        // log
473        if (logScaling)
474            MathUtil.log(newHistogramData);
475        // normalize data
476        MathUtil.normalize(newHistogramData);
477
478        // get new data cache and apply min, max, integer type
479        histogramData = newHistogramData;
480        minValue = getMinValue();
481        maxValue = getMaxValue();
482        integer = isIntegerType();
483
484        // request repaint
485        repaint();
486    }
487
488    /**
489     * Returns the ratio to convert a data value to corresponding pixel X position
490     */
491    protected double getDataToPixelRatio()
492    {
493        final double pixelRange = Math.max(getClientWidth() - 1, 32);
494        final double dataRange = maxValue - minValue;
495
496        if (dataRange != 0d)
497            return pixelRange / dataRange;
498
499        return 0d;
500    }
501
502    /**
503     * Returns the ratio to convert a pixel X position to corresponding data value
504     */
505    protected double getPixelToDataRatio()
506    {
507        final double pixelRange = Math.max(getClientWidth() - 1, 32);
508        final double dataRange = maxValue - minValue;
509
510        if (pixelRange != 0d)
511            return dataRange / pixelRange;
512
513        return 0d;
514    }
515
516    /**
517     * Returns the ratio to convert a pixel X position to corresponding histo bin
518     */
519    protected double getPixelToHistoRatio()
520    {
521        final double histogramRange = histogramData.length - 1;
522        final double pixelRange = Math.max(getClientWidth() - 1, 32);
523
524        if (pixelRange != 0d)
525            return histogramRange / pixelRange;
526
527        return 0d;
528    }
529
530    /**
531     * Convert a data value to the corresponding pixel position
532     */
533    public int dataToPixel(double value)
534    {
535        return (int) Math.round(((value - minValue) * getDataToPixelRatio())) + getClientX();
536    }
537
538    /**
539     * Convert a pixel position to corresponding data value
540     */
541    public double pixelToData(int value)
542    {
543        final double data = ((value - getClientX()) * getPixelToDataRatio()) + minValue;
544        return Math.min(Math.max(data, minValue), maxValue);
545    }
546
547    /**
548     * Convert a pixel position to corresponding bin index
549     */
550    public int pixelToBin(int value)
551    {
552        final int index = (int) Math.round((value - getClientX()) * getPixelToHistoRatio());
553        return Math.min(Math.max(index, 0), histogramData.length - 1);
554    }
555
556    /**
557     * Notify all listeners that histogram need to be recomputed
558     */
559    protected void fireHistogramNeedRefresh()
560    {
561        for (HistogramPanelListener l : listenerList.getListeners(HistogramPanelListener.class))
562            l.histogramNeedRefresh(this);
563    }
564
565    public void addListener(HistogramPanelListener l)
566    {
567        listenerList.add(HistogramPanelListener.class, l);
568    }
569
570    public void removeListener(HistogramPanelListener l)
571    {
572        listenerList.remove(HistogramPanelListener.class, l);
573    }
574
575    @Override
576    protected void paintComponent(Graphics g)
577    {
578        final Color fc;
579        final Color bc;
580
581        if (useLAFColors)
582        {
583            fc = getForeground();
584            bc = getBackground();
585        }
586        else
587        {
588            fc = color;
589            bc = backgroundColor;
590        }
591
592        final Graphics2D g2 = (Graphics2D) g.create();
593
594        g2.setColor(fc);
595        g2.setBackground(bc);
596
597        // background color
598        if (isOpaque())
599            g2.clearRect(0, 0, getWidth(), getHeight());
600
601        // data cache
602        final double ratio = getPixelToHistoRatio();
603        final double[] data = histogramData;
604
605        // not yet computed
606        if (data.length != 0)
607        {
608            final int histoRange = data.length - 1;
609            final int hRange = getClientHeight() - 1;
610            final int bottom = getClientY() + hRange;
611            final int l = getClientX();
612            final int r = l + getClientWidth();
613
614            for (int i = l; i < r; i++)
615            {
616                int index = (int) Math.round((i - l) * ratio);
617
618                if (index < 0)
619                    index = 0;
620                else if (index > histoRange)
621                    index = histoRange;
622
623                g2.drawLine(i, bottom, i, bottom - (int) Math.round(data[index] * hRange));
624            }
625        }
626
627        if ((data.length == 0) || updating)
628        {
629            final int x = (getWidth() / 2) - 60;
630            final int y = (getHeight() / 2) - 20;
631
632            g2.drawString("computing...", x, y);
633        }
634
635        g2.dispose();
636
637        // just check for histogram properties change
638        checkHisto();
639    }
640}