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}