001/** 002 * 003 */ 004package icy.image.colormap; 005 006import icy.file.xml.XMLPersistent; 007import icy.math.Interpolator; 008import icy.util.XMLUtil; 009 010import java.awt.Point; 011import java.util.ArrayList; 012import java.util.Arrays; 013import java.util.Collections; 014import java.util.List; 015 016import org.w3c.dom.Element; 017import org.w3c.dom.Node; 018 019/** 020 * @author Stephane 021 */ 022public class IcyColorMapComponent implements XMLPersistent 023{ 024 static final String ID_INDEX = "index"; 025 static final String ID_VALUE = "value"; 026 027 public class ControlPoint implements Comparable<ControlPoint>, XMLPersistent 028 { 029 int index; 030 int value; 031 private final boolean fixed; 032 033 /** 034 * @param index 035 * @param value 036 */ 037 public ControlPoint(int index, int value, boolean fixed) 038 { 039 super(); 040 041 this.index = index; 042 this.value = value; 043 this.fixed = fixed; 044 } 045 046 /** 047 * @param index 048 * @param value 049 */ 050 public ControlPoint(int index, int value) 051 { 052 this(index, value, false); 053 } 054 055 /** 056 * @return the fixed flag 057 */ 058 public boolean isFixed() 059 { 060 return fixed; 061 } 062 063 /** 064 * @return the index 065 */ 066 public int getIndex() 067 { 068 return index; 069 } 070 071 /** 072 * @param index 073 * the index to set 074 */ 075 public void setIndex(int index) 076 { 077 if ((!fixed) && (this.index != index)) 078 { 079 this.index = index; 080 081 changed(); 082 } 083 } 084 085 /** 086 * @return the value 087 */ 088 public int getValue() 089 { 090 return value; 091 } 092 093 /** 094 * @param value 095 * the value to set 096 */ 097 public void setValue(int value) 098 { 099 if (this.value != value) 100 { 101 this.value = value; 102 103 changed(); 104 } 105 } 106 107 /** 108 * Set control point position 109 * 110 * @param p 111 * point 112 */ 113 public void setPosition(Point p) 114 { 115 setPosition(p.x, p.y); 116 } 117 118 /** 119 * Get control point position 120 * 121 * @return point position 122 */ 123 public Point getPosition() 124 { 125 return new Point(index, value); 126 } 127 128 /** 129 * Set control point position 130 * 131 * @param index 132 * @param value 133 */ 134 public void setPosition(int index, int value) 135 { 136 if (((!fixed) && (this.index != index)) || (this.value != value)) 137 { 138 if (!fixed) 139 this.index = index; 140 this.value = value; 141 142 changed(); 143 } 144 } 145 146 /** 147 * remove the control point 148 */ 149 public void remove() 150 { 151 if (!fixed) 152 removeControlPoint(this); 153 } 154 155 /** 156 * put here process on changed event 157 */ 158 protected void onChanged() 159 { 160 // nothing for now 161 162 } 163 164 /** 165 * changed event 166 */ 167 protected void changed() 168 { 169 // common process on change 170 onChanged(); 171 // inform colormap that control point has changed 172 controlPointChanged(this); 173 } 174 175 @Override 176 public int compareTo(ControlPoint o) 177 { 178 if (index < o.getIndex()) 179 return -1; 180 else if (index > o.getIndex()) 181 return 1; 182 else 183 return 0; 184 } 185 186 @Override 187 public boolean loadFromXML(Node node) 188 { 189 if (node == null) 190 return false; 191 192 final int ind = XMLUtil.getElementIntValue(node, ID_INDEX, 0); 193 final int val = XMLUtil.getElementIntValue(node, ID_VALUE, 0); 194 195 setPosition(ind, val); 196 197 return true; 198 } 199 200 @Override 201 public boolean saveToXML(Node node) 202 { 203 if (node == null) 204 return false; 205 206 XMLUtil.setElementIntValue(node, ID_INDEX, getIndex()); 207 XMLUtil.setElementIntValue(node, ID_VALUE, getValue()); 208 209 return true; 210 } 211 212 @Override 213 public boolean equals(Object obj) 214 { 215 if (obj instanceof ControlPoint) 216 { 217 final ControlPoint pt = (ControlPoint) obj; 218 return (index == pt.index) && (value == pt.value); 219 } 220 221 return super.equals(obj); 222 } 223 224 @Override 225 public int hashCode() 226 { 227 return index ^ value; 228 } 229 } 230 231 private static final String ID_RAWDATA = "rawdata"; 232 private static final String ID_POINT = "point"; 233 234 /** 235 * parent colormap 236 */ 237 private final IcyColorMap colormap; 238 /** 239 * list of control point 240 */ 241 protected final ArrayList<ControlPoint> controlPoints; 242 /** 243 * we use short to store byte to avoid "sign problem" 244 */ 245 public final short[] map; 246 /** 247 * normalized maps 248 */ 249 public final float[] mapf; 250 251 /** 252 * internals 253 */ 254 private int updateCnt; 255 private boolean controlPointsChangedPending; 256 private boolean mapDataChangedPending; 257 private boolean mapFDataChangedPending; 258 private boolean rawData; 259 260 public IcyColorMapComponent(IcyColorMap colorMap, short initValue) 261 { 262 super(); 263 264 controlPoints = new ArrayList<ControlPoint>(); 265 266 this.colormap = colorMap; 267 268 // allocate map 269 map = new short[IcyColorMap.SIZE]; 270 mapf = new float[IcyColorMap.SIZE]; 271 272 // default 273 Arrays.fill(map, initValue); 274 updateFloatMapFromIntMap(); 275 276 // add fixed control point to index 0 277 controlPoints.add(new ControlPoint(0, map[0], true)); 278 // add fixed control point to index IcyColorMap.MAX_INDEX 279 controlPoints.add(new ControlPoint(IcyColorMap.MAX_INDEX, map[IcyColorMap.MAX_INDEX], true)); 280 281 updateCnt = 0; 282 controlPointsChangedPending = false; 283 mapDataChangedPending = false; 284 mapFDataChangedPending = false; 285 rawData = false; 286 } 287 288 public IcyColorMapComponent(IcyColorMap colorMap) 289 { 290 this(colorMap, (short) 0); 291 } 292 293 public int getControlPointCount() 294 { 295 return controlPoints.size(); 296 } 297 298 public ArrayList<ControlPoint> getControlPoints() 299 { 300 return controlPoints; 301 } 302 303 /** 304 * get the control point 305 */ 306 public ControlPoint getControlPoint(int index) 307 { 308 return controlPoints.get(index); 309 } 310 311 /** 312 * get the control point at specified index (return null if not found) 313 */ 314 public ControlPoint getControlPointWithIndex(int index, boolean create) 315 { 316 // TODO: search can be optimized as the list is sorted on index value 317 for (ControlPoint cp : controlPoints) 318 if (cp.getIndex() == index) 319 return cp; 320 321 if (create) 322 { 323 final ControlPoint result = new ControlPoint(index, 0, (index == 0) || (index == IcyColorMap.MAX_INDEX)); 324 // add to list 325 controlPoints.add(result); 326 // and return 327 return result; 328 } 329 330 return null; 331 } 332 333 /** 334 * Return true if there is a control point at specified index 335 */ 336 public boolean hasControlPointWithIndex(int index) 337 { 338 return getControlPointWithIndex(index, false) != null; 339 } 340 341 /** 342 * Set a control point to specified index and value (normalized) 343 */ 344 public ControlPoint setControlPoint(int index, float value) 345 { 346 return setControlPoint(index, (int) (value * IcyColorMap.MAX_LEVEL)); 347 } 348 349 /** 350 * Set a control point to specified index and value 351 */ 352 public ControlPoint setControlPoint(int index, int value) 353 { 354 // flag to indicate we don't have raw data 355 rawData = false; 356 357 // search for an existing control point at this index 358 ControlPoint controlPoint = getControlPointWithIndex(index, false); 359 360 // not found ? 361 if (controlPoint == null) 362 { 363 // create a new control point 364 controlPoint = new ControlPoint(index, value); 365 // and add it to the list 366 controlPoints.add(controlPoint); 367 // notify point added 368 controlPointAdded(controlPoint); 369 } 370 else 371 { 372 // modify intensity of control point 373 controlPoint.setValue(value); 374 } 375 376 return controlPoint; 377 } 378 379 /** 380 * Remove the specified control point 381 * 382 * @param controlPoint 383 */ 384 public void removeControlPoint(ControlPoint controlPoint) 385 { 386 if (controlPoints.remove(controlPoint)) 387 controlPointRemoved(controlPoint); 388 } 389 390 /** 391 * Remove all control point 392 */ 393 public void removeAllControlPoint() 394 { 395 if (controlPoints.size() <= 2) 396 return; 397 398 beginUpdate(); 399 try 400 { 401 // more than the 2 fixed controls point ? 402 while (controlPoints.size() > 2) 403 removeControlPoint(controlPoints.get(1)); 404 } 405 finally 406 { 407 endUpdate(); 408 } 409 } 410 411 /** 412 * Copy data from specified source colormap band 413 */ 414 public void copyFrom(IcyColorMapComponent source) 415 { 416 // copy the rawData property 417 rawData = source.rawData; 418 419 // we remove all controls points (even fixed ones) 420 controlPoints.clear(); 421 422 for (ControlPoint cp : source.controlPoints) 423 controlPoints.add(new ControlPoint(cp.getIndex(), cp.getValue(), cp.isFixed())); 424 425 // only the 2 fixed controls point ? 426 if (controlPoints.size() <= 2) 427 { 428 // directly copy table data 429 System.arraycopy(source.map, 0, map, 0, IcyColorMap.SIZE); 430 // notify we changed table data 431 mapDataChanged(); 432 } 433 else 434 // notify we modified control point 435 controlPointsChanged(); 436 } 437 438 /** 439 * Copy data from specified byte array 440 */ 441 public void copyFrom(byte[] src) 442 { 443 // we remove all controls points (even fixed ones) 444 controlPoints.clear(); 445 446 final double srcOffsetStep = src.length / IcyColorMap.SIZE; 447 double srcOffset = 0; 448 449 // directly copy table data 450 for (int dstOffset = 0; dstOffset < IcyColorMap.SIZE; dstOffset++) 451 { 452 map[dstOffset] = (short) (src[(int) srcOffset] & 0xFF); 453 srcOffset += srcOffsetStep; 454 } 455 456 // take it as this is a raw map 457 rawData = true; 458 459 // notify we changed table data 460 mapDataChanged(); 461 } 462 463 /** 464 * Copy data from specified short array.<br> 465 * 466 * @param src 467 * data short array 468 * @param shift 469 * shift factor if value need to be shifted (8 if data are short formatted) 470 */ 471 public void copyFrom(short[] src, int shift) 472 { 473 final byte[] byteMap = new byte[src.length]; 474 475 // transform short map to byte map 476 for (int i = 0; i < src.length; i++) 477 byteMap[i] = (byte) (src[i] >> shift); 478 479 // copy 480 copyFrom(byteMap); 481 } 482 483 /** 484 * Returns colormap content as an array of byte (length = IcyColorMap.SIZE). 485 */ 486 public byte[] asByteArray() 487 { 488 final byte[] result = new byte[IcyColorMap.SIZE]; 489 490 for (int i = 0; i < result.length; i++) 491 result[i] = (byte) getValue(i); 492 493 return result; 494 } 495 496 /** 497 * Return value for specified index 498 */ 499 public short getValue(int index) 500 { 501 return map[index]; 502 } 503 504 /** 505 * @deprecated Use {@link #getValue(int)} instead. 506 */ 507 @Deprecated 508 public short getIntensity(int index) 509 { 510 return getValue(index); 511 } 512 513 /** 514 * Set direct intensity value to specified index 515 */ 516 public void setValue(int index, int value) 517 { 518 // flag to indicate we have raw data 519 rawData = true; 520 521 if (map[index] != value) 522 { 523 // clear control point as we are manually setting map value 524 removeAllControlPoint(); 525 526 // set value 527 map[index] = (short) value; 528 529 // notify change 530 mapDataChanged(); 531 } 532 } 533 534 /** 535 * Set direct intensity (normalized) value to specified index 536 */ 537 public void setNormalizedValue(int index, float value) 538 { 539 // flag to indicate we have raw data 540 rawData = true; 541 542 if (mapf[index] != value) 543 { 544 // clear control point as we are manually setting map value 545 removeAllControlPoint(); 546 547 // set value 548 mapf[index] = value; 549 550 // notify change 551 mapFDataChanged(); 552 } 553 } 554 555 /** 556 * Return true is the color map band is all set to a fixed value. 557 */ 558 public boolean isAllSame() 559 { 560 final short value = map[0]; 561 562 for (int i = 1; i < IcyColorMap.SIZE; i++) 563 if (map[i] != value) 564 return false; 565 566 return true; 567 } 568 569 /** 570 * Return true is the color map band is all set to zero. 571 */ 572 public boolean isAllZero() 573 { 574 for (short value : map) 575 if (value != 0) 576 return false; 577 578 return true; 579 } 580 581 /** 582 * Return true is the color map band is all set to one. 583 */ 584 public boolean isAllOne() 585 { 586 for (short value : map) 587 if (value != IcyColorMap.MAX_LEVEL) 588 return false; 589 590 return true; 591 } 592 593 /** 594 * Return true is the color map band is a linear one.<br> 595 * Linear map are used to display plain gray or plain color image.<br> 596 * Non linear map means you may have an indexed color image or 597 * you want to enhance contrast/color in display. 598 */ 599 public boolean isLinear() 600 { 601 float lastdiff = mapf[1] - mapf[0]; 602 603 for (int i = 2; i < IcyColorMap.SIZE; i++) 604 { 605 final float diff = mapf[i] - mapf[i - 1]; 606 607 if ((diff == 0) || (lastdiff == 0)) 608 continue; 609 610 // difference changed ? 611 if ((diff != lastdiff) && (Math.abs(diff / (diff - lastdiff)) < 1000f)) 612 return false; 613 614 lastdiff = diff; 615 } 616 617 return true; 618 } 619 620 /** 621 * update float map from int map 622 */ 623 private void updateFloatMapFromIntMap() 624 { 625 for (int i = 0; i < IcyColorMap.SIZE; i++) 626 mapf[i] = (float) map[i] / IcyColorMap.MAX_LEVEL; 627 } 628 629 /** 630 * update float map from int map 631 */ 632 private void updateIntMapFromFloatMap() 633 { 634 for (int i = 0; i < IcyColorMap.SIZE; i++) 635 map[i] = (short) (mapf[i] * IcyColorMap.MAX_LEVEL); 636 } 637 638 /** 639 * update fixed controls points with map data 640 */ 641 private void updateFixedCP() 642 { 643 // internal update (no event wanted) 644 getControlPointWithIndex(0, true).value = map[0]; 645 getControlPointWithIndex(IcyColorMap.MAX_INDEX, true).value = map[IcyColorMap.MAX_INDEX]; 646 } 647 648 /** 649 * Called when a control point has been modified 650 * 651 * @param controlPoint 652 * modified control point 653 */ 654 public void controlPointChanged(ControlPoint controlPoint) 655 { 656 controlPointsChanged(); 657 } 658 659 /** 660 * Called when a control point has been added 661 * 662 * @param controlPoint 663 * added control point 664 */ 665 public void controlPointAdded(ControlPoint controlPoint) 666 { 667 controlPointsChanged(); 668 } 669 670 /** 671 * Called when a control point has been removed 672 * 673 * @param controlPoint 674 * removed control point 675 */ 676 public void controlPointRemoved(ControlPoint controlPoint) 677 { 678 controlPointsChanged(); 679 } 680 681 /** 682 * common process on Control Point list change 683 */ 684 public void onControlPointsChanged() 685 { 686 // sort the list 687 Collections.sort(controlPoints); 688 689 final List<Point> points = new ArrayList<Point>(); 690 691 // get position only 692 for (ControlPoint point : controlPoints) 693 points.add(point.getPosition()); 694 695 // get linear interpolation values 696 final double[] values = Interpolator.doYLinearInterpolation(points, 1); 697 698 // directly modify the colormap table data 699 for (int i = 0; i < IcyColorMap.SIZE; i++) 700 map[i] = (short) Math.round(values[i]); 701 702 mapDataChanged(); 703 } 704 705 /** 706 * common process on map (int) data change 707 */ 708 public void onMapDataChanged() 709 { 710 // update float map from the modified int map 711 updateFloatMapFromIntMap(); 712 // update fixed controls points 713 updateFixedCP(); 714 715 // take it as this is a raw map 716 if (rawData) 717 rawData = !isLinear(); 718 719 // manually set a changed event as we directly modified the colormap 720 colormap.changed(); 721 } 722 723 /** 724 * common process on map (float) data change 725 */ 726 public void onMapFDataChanged() 727 { 728 // update int map from the modified float map 729 updateIntMapFromFloatMap(); 730 // udpate fixed controls points 731 updateFixedCP(); 732 // manually set a changed event as we directly modified the colormap 733 colormap.changed(); 734 } 735 736 /** 737 * called when the controller modified Control Point list 738 */ 739 public void controlPointsChanged() 740 { 741 if (isUpdating()) 742 { 743 controlPointsChangedPending = true; 744 // map will be modified anyway 745 mapDataChangedPending = false; 746 mapFDataChangedPending = false; 747 } 748 else 749 onControlPointsChanged(); 750 } 751 752 /** 753 * called when the controller directly modified the map (int) data 754 */ 755 public void mapDataChanged() 756 { 757 if (isUpdating()) 758 { 759 mapDataChangedPending = true; 760 // to keep the changed made to map (int) 761 mapFDataChangedPending = false; 762 controlPointsChangedPending = false; 763 } 764 else 765 onMapDataChanged(); 766 } 767 768 /** 769 * called when the controller directly modified the map (float) data 770 */ 771 public void mapFDataChanged() 772 { 773 if (isUpdating()) 774 { 775 mapFDataChangedPending = true; 776 // to keep the changed made to map (float) 777 mapDataChangedPending = false; 778 controlPointsChangedPending = false; 779 } 780 else 781 onMapFDataChanged(); 782 } 783 784 public void beginUpdate() 785 { 786 updateCnt++; 787 } 788 789 public void endUpdate() 790 { 791 updateCnt--; 792 if (updateCnt <= 0) 793 { 794 // process pending tasks 795 if (controlPointsChangedPending) 796 { 797 onControlPointsChanged(); 798 controlPointsChangedPending = false; 799 } 800 else if (mapDataChangedPending) 801 { 802 onMapDataChanged(); 803 mapDataChangedPending = false; 804 } 805 else if (mapFDataChangedPending) 806 { 807 onMapFDataChanged(); 808 mapFDataChangedPending = false; 809 } 810 } 811 } 812 813 public boolean isUpdating() 814 { 815 return updateCnt > 0; 816 } 817 818 /** 819 * returns true when the LUT is specified by raw data (for example GIF files), 820 * false when the LUT is specified by control points. 821 */ 822 public boolean isRawData() 823 { 824 return rawData; 825 } 826 827 @Override 828 public boolean loadFromXML(Node node) 829 { 830 if (node == null) 831 return false; 832 833 rawData = XMLUtil.getAttributeBooleanValue((Element) node, ID_RAWDATA, false); 834 835 final List<Node> nodesPoint = XMLUtil.getChildren(node, ID_POINT); 836 837 beginUpdate(); 838 try 839 { 840 if (rawData) 841 { 842 int ind = 0; 843 if (nodesPoint.size() == 0) 844 { 845 final byte[] data = XMLUtil.getElementBytesValue(node, ID_VALUE, new byte[] {}); 846 847 // an error occurred while retrieved XML data 848 if (data == null) 849 return false; 850 851 copyFrom(data); 852 } 853 else 854 { 855 // backward compatibility 856 for (Node nodePoint : nodesPoint) 857 { 858 final int val = XMLUtil.getElementIntValue(nodePoint, ID_VALUE, 0); 859 860 setValue(ind, val); 861 ind++; 862 } 863 } 864 } 865 else 866 { 867 removeAllControlPoint(); 868 for (Node nodePoint : nodesPoint) 869 { 870 final int ind = XMLUtil.getElementIntValue(nodePoint, ID_INDEX, 0); 871 final int val = XMLUtil.getElementIntValue(nodePoint, ID_VALUE, 0); 872 873 setControlPoint(ind, val); 874 } 875 } 876 } 877 finally 878 { 879 endUpdate(); 880 } 881 882 return true; 883 } 884 885 @Override 886 public boolean saveToXML(Node node) 887 { 888 if (node == null) 889 return false; 890 891 XMLUtil.setAttributeBooleanValue((Element) node, ID_RAWDATA, rawData); 892 XMLUtil.removeChildren(node, ID_POINT); 893 894 boolean result = true; 895 896 if (rawData) 897 { 898 XMLUtil.removeChildren(node, ID_VALUE); 899 XMLUtil.setElementBytesValue(node, ID_VALUE, asByteArray()); 900 } 901 else 902 { 903 for (int ind = 0; ind < controlPoints.size(); ind++) 904 { 905 final ControlPoint cp = controlPoints.get(ind); 906 final Node nodePoint = XMLUtil.addElement(node, ID_POINT); 907 908 result = result && cp.saveToXML(nodePoint); 909 } 910 } 911 912 return result; 913 } 914 915 @Override 916 public boolean equals(Object obj) 917 { 918 if (obj instanceof IcyColorMapComponent) 919 // just compare the map content (we don't care about control point here) 920 return Arrays.equals(map, ((IcyColorMapComponent) obj).map); 921 922 return super.equals(obj); 923 } 924 925 @Override 926 public int hashCode() 927 { 928 return map.hashCode(); 929 } 930}