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}