/** * */ package icy.image.colormap; import icy.file.xml.XMLPersistent; import icy.math.Interpolator; import icy.util.XMLUtil; import java.awt.Point; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; import org.w3c.dom.Element; import org.w3c.dom.Node; /** * @author Stephane */ public class IcyColorMapComponent implements XMLPersistent { static final String ID_INDEX = "index"; static final String ID_VALUE = "value"; public class ControlPoint implements Comparable<ControlPoint>, XMLPersistent { int index; int value; private final boolean fixed; /** * @param index * @param value */ public ControlPoint(int index, int value, boolean fixed) { super(); this.index = index; this.value = value; this.fixed = fixed; } /** * @param index * @param value */ public ControlPoint(int index, int value) { this(index, value, false); } /** * @return the fixed flag */ public boolean isFixed() { return fixed; } /** * @return the index */ public int getIndex() { return index; } /** * @param index * the index to set */ public void setIndex(int index) { if ((!fixed) && (this.index != index)) { this.index = index; changed(); } } /** * @return the value */ public int getValue() { return value; } /** * @param value * the value to set */ public void setValue(int value) { if (this.value != value) { this.value = value; changed(); } } /** * Set control point position * * @param p * point */ public void setPosition(Point p) { setPosition(p.x, p.y); } /** * Get control point position * * @return point position */ public Point getPosition() { return new Point(index, value); } /** * Set control point position * * @param index * @param value */ public void setPosition(int index, int value) { if (((!fixed) && (this.index != index)) || (this.value != value)) { if (!fixed) this.index = index; this.value = value; changed(); } } /** * remove the control point */ public void remove() { if (!fixed) removeControlPoint(this); } /** * put here process on changed event */ protected void onChanged() { // nothing for now } /** * changed event */ protected void changed() { // common process on change onChanged(); // inform colormap that control point has changed controlPointChanged(this); } @Override public int compareTo(ControlPoint o) { if (index < o.getIndex()) return -1; else if (index > o.getIndex()) return 1; else return 0; } @Override public boolean loadFromXML(Node node) { if (node == null) return false; final int ind = XMLUtil.getElementIntValue(node, ID_INDEX, 0); final int val = XMLUtil.getElementIntValue(node, ID_VALUE, 0); setPosition(ind, val); return true; } @Override public boolean saveToXML(Node node) { if (node == null) return false; XMLUtil.setElementIntValue(node, ID_INDEX, getIndex()); XMLUtil.setElementIntValue(node, ID_VALUE, getValue()); return true; } @Override public boolean equals(Object obj) { if (obj instanceof ControlPoint) { final ControlPoint pt = (ControlPoint) obj; return (index == pt.index) && (value == pt.value); } return super.equals(obj); } @Override public int hashCode() { return index ^ value; } } private static final String ID_RAWDATA = "rawdata"; private static final String ID_POINT = "point"; /** * parent colormap */ private final IcyColorMap colormap; /** * list of control point */ protected final ArrayList<ControlPoint> controlPoints; /** * we use short to store byte to avoid "sign problem" */ public final short[] map; /** * normalized maps */ public final float[] mapf; /** * internals */ private int updateCnt; private boolean controlPointsChangedPending; private boolean mapDataChangedPending; private boolean mapFDataChangedPending; private boolean rawData; public IcyColorMapComponent(IcyColorMap colorMap, short initValue) { super(); controlPoints = new ArrayList<ControlPoint>(); this.colormap = colorMap; // allocate map map = new short[IcyColorMap.SIZE]; mapf = new float[IcyColorMap.SIZE]; // default Arrays.fill(map, initValue); updateFloatMapFromIntMap(); // add fixed control point to index 0 controlPoints.add(new ControlPoint(0, map[0], true)); // add fixed control point to index IcyColorMap.MAX_INDEX controlPoints.add(new ControlPoint(IcyColorMap.MAX_INDEX, map[IcyColorMap.MAX_INDEX], true)); updateCnt = 0; controlPointsChangedPending = false; mapDataChangedPending = false; mapFDataChangedPending = false; rawData = false; } public IcyColorMapComponent(IcyColorMap colorMap) { this(colorMap, (short) 0); } public int getControlPointCount() { return controlPoints.size(); } public ArrayList<ControlPoint> getControlPoints() { return controlPoints; } /** * get the control point */ public ControlPoint getControlPoint(int index) { return controlPoints.get(index); } /** * get the control point at specified index (return null if not found) */ public ControlPoint getControlPointWithIndex(int index, boolean create) { // TODO: search can be optimized as the list is sorted on index value for (ControlPoint cp : controlPoints) if (cp.getIndex() == index) return cp; if (create) { final ControlPoint result = new ControlPoint(index, 0, (index == 0) || (index == IcyColorMap.MAX_INDEX)); // add to list controlPoints.add(result); // and return return result; } return null; } /** * Return true if there is a control point at specified index */ public boolean hasControlPointWithIndex(int index) { return getControlPointWithIndex(index, false) != null; } /** * Set a control point to specified index and value (normalized) */ public ControlPoint setControlPoint(int index, float value) { return setControlPoint(index, (int) (value * IcyColorMap.MAX_LEVEL)); } /** * Set a control point to specified index and value */ public ControlPoint setControlPoint(int index, int value) { // flag to indicate we don't have raw data rawData = false; // search for an existing control point at this index ControlPoint controlPoint = getControlPointWithIndex(index, false); // not found ? if (controlPoint == null) { // create a new control point controlPoint = new ControlPoint(index, value); // and add it to the list controlPoints.add(controlPoint); // notify point added controlPointAdded(controlPoint); } else { // modify intensity of control point controlPoint.setValue(value); } return controlPoint; } /** * Remove the specified control point * * @param controlPoint */ public void removeControlPoint(ControlPoint controlPoint) { if (controlPoints.remove(controlPoint)) controlPointRemoved(controlPoint); } /** * Remove all control point */ public void removeAllControlPoint() { if (controlPoints.size() <= 2) return; beginUpdate(); try { // more than the 2 fixed controls point ? while (controlPoints.size() > 2) removeControlPoint(controlPoints.get(1)); } finally { endUpdate(); } } /** * Copy data from specified source colormap band */ public void copyFrom(IcyColorMapComponent source) { // copy the rawData property rawData = source.rawData; // we remove all controls points (even fixed ones) controlPoints.clear(); for (ControlPoint cp : source.controlPoints) controlPoints.add(new ControlPoint(cp.getIndex(), cp.getValue(), cp.isFixed())); // only the 2 fixed controls point ? if (controlPoints.size() <= 2) { // directly copy table data System.arraycopy(source.map, 0, map, 0, IcyColorMap.SIZE); // notify we changed table data mapDataChanged(); } else // notify we modified control point controlPointsChanged(); } /** * Copy data from specified byte array */ public void copyFrom(byte[] src) { // we remove all controls points (even fixed ones) controlPoints.clear(); final double srcOffsetStep = src.length / IcyColorMap.SIZE; double srcOffset = 0; // directly copy table data for (int dstOffset = 0; dstOffset < IcyColorMap.SIZE; dstOffset++) { map[dstOffset] = (short) (src[(int) srcOffset] & 0xFF); srcOffset += srcOffsetStep; } // take it as this is a raw map rawData = true; // notify we changed table data mapDataChanged(); } /** * Copy data from specified short array.<br> * * @param src * data short array * @param shift * shift factor if value need to be shifted (8 if data are short formatted) */ public void copyFrom(short[] src, int shift) { final byte[] byteMap = new byte[src.length]; // transform short map to byte map for (int i = 0; i < src.length; i++) byteMap[i] = (byte) (src[i] >> shift); // copy copyFrom(byteMap); } /** * Returns colormap content as an array of byte (length = IcyColorMap.SIZE). */ public byte[] asByteArray() { final byte[] result = new byte[IcyColorMap.SIZE]; for (int i = 0; i < result.length; i++) result[i] = (byte) getValue(i); return result; } /** * Return value for specified index */ public short getValue(int index) { return map[index]; } /** * @deprecated Use {@link #getValue(int)} instead. */ @Deprecated public short getIntensity(int index) { return getValue(index); } /** * Set direct intensity value to specified index */ public void setValue(int index, int value) { // flag to indicate we have raw data rawData = true; if (map[index] != value) { // clear control point as we are manually setting map value removeAllControlPoint(); // set value map[index] = (short) value; // notify change mapDataChanged(); } } /** * Set direct intensity (normalized) value to specified index */ public void setNormalizedValue(int index, float value) { // flag to indicate we have raw data rawData = true; if (mapf[index] != value) { // clear control point as we are manually setting map value removeAllControlPoint(); // set value mapf[index] = value; // notify change mapFDataChanged(); } } /** * Return true is the color map band is all set to a fixed value. */ public boolean isAllSame() { final short value = map[0]; for (int i = 1; i < IcyColorMap.SIZE; i++) if (map[i] != value) return false; return true; } /** * Return true is the color map band is all set to zero. */ public boolean isAllZero() { for (short value : map) if (value != 0) return false; return true; } /** * Return true is the color map band is all set to one. */ public boolean isAllOne() { for (short value : map) if (value != IcyColorMap.MAX_LEVEL) return false; return true; } /** * Return true is the color map band is a linear one.<br> * Linear map are used to display plain gray or plain color image.<br> * Non linear map means you may have an indexed color image or * you want to enhance contrast/color in display. */ public boolean isLinear() { float lastdiff = mapf[1] - mapf[0]; for (int i = 2; i < IcyColorMap.SIZE; i++) { final float diff = mapf[i] - mapf[i - 1]; if ((diff == 0) || (lastdiff == 0)) continue; // difference changed ? if ((diff != lastdiff) && (Math.abs(diff / (diff - lastdiff)) < 1000f)) return false; lastdiff = diff; } return true; } /** * update float map from int map */ private void updateFloatMapFromIntMap() { for (int i = 0; i < IcyColorMap.SIZE; i++) mapf[i] = (float) map[i] / IcyColorMap.MAX_LEVEL; } /** * update float map from int map */ private void updateIntMapFromFloatMap() { for (int i = 0; i < IcyColorMap.SIZE; i++) map[i] = (short) (mapf[i] * IcyColorMap.MAX_LEVEL); } /** * update fixed controls points with map data */ private void updateFixedCP() { // internal update (no event wanted) getControlPointWithIndex(0, true).value = map[0]; getControlPointWithIndex(IcyColorMap.MAX_INDEX, true).value = map[IcyColorMap.MAX_INDEX]; } /** * Called when a control point has been modified * * @param controlPoint * modified control point */ public void controlPointChanged(ControlPoint controlPoint) { controlPointsChanged(); } /** * Called when a control point has been added * * @param controlPoint * added control point */ public void controlPointAdded(ControlPoint controlPoint) { controlPointsChanged(); } /** * Called when a control point has been removed * * @param controlPoint * removed control point */ public void controlPointRemoved(ControlPoint controlPoint) { controlPointsChanged(); } /** * common process on Control Point list change */ public void onControlPointsChanged() { // sort the list Collections.sort(controlPoints); final List<Point> points = new ArrayList<Point>(); // get position only for (ControlPoint point : controlPoints) points.add(point.getPosition()); // get linear interpolation values final double[] values = Interpolator.doYLinearInterpolation(points, 1); // directly modify the colormap table data for (int i = 0; i < IcyColorMap.SIZE; i++) map[i] = (short) Math.round(values[i]); mapDataChanged(); } /** * common process on map (int) data change */ public void onMapDataChanged() { // update float map from the modified int map updateFloatMapFromIntMap(); // update fixed controls points updateFixedCP(); // take it as this is a raw map if (rawData) rawData = !isLinear(); // manually set a changed event as we directly modified the colormap colormap.changed(); } /** * common process on map (float) data change */ public void onMapFDataChanged() { // update int map from the modified float map updateIntMapFromFloatMap(); // udpate fixed controls points updateFixedCP(); // manually set a changed event as we directly modified the colormap colormap.changed(); } /** * called when the controller modified Control Point list */ public void controlPointsChanged() { if (isUpdating()) { controlPointsChangedPending = true; // map will be modified anyway mapDataChangedPending = false; mapFDataChangedPending = false; } else onControlPointsChanged(); } /** * called when the controller directly modified the map (int) data */ public void mapDataChanged() { if (isUpdating()) { mapDataChangedPending = true; // to keep the changed made to map (int) mapFDataChangedPending = false; controlPointsChangedPending = false; } else onMapDataChanged(); } /** * called when the controller directly modified the map (float) data */ public void mapFDataChanged() { if (isUpdating()) { mapFDataChangedPending = true; // to keep the changed made to map (float) mapDataChangedPending = false; controlPointsChangedPending = false; } else onMapFDataChanged(); } public void beginUpdate() { updateCnt++; } public void endUpdate() { updateCnt--; if (updateCnt <= 0) { // process pending tasks if (controlPointsChangedPending) { onControlPointsChanged(); controlPointsChangedPending = false; } else if (mapDataChangedPending) { onMapDataChanged(); mapDataChangedPending = false; } else if (mapFDataChangedPending) { onMapFDataChanged(); mapFDataChangedPending = false; } } } public boolean isUpdating() { return updateCnt > 0; } /** * returns true when the LUT is specified by raw data (for example GIF files), * false when the LUT is specified by control points. */ public boolean isRawData() { return rawData; } @Override public boolean loadFromXML(Node node) { if (node == null) return false; rawData = XMLUtil.getAttributeBooleanValue((Element) node, ID_RAWDATA, false); final List<Node> nodesPoint = XMLUtil.getChildren(node, ID_POINT); beginUpdate(); try { if (rawData) { int ind = 0; if (nodesPoint.size() == 0) { final byte[] data = XMLUtil.getElementBytesValue(node, ID_VALUE, new byte[] {}); // an error occurred while retrieved XML data if (data == null) return false; copyFrom(data); } else { // backward compatibility for (Node nodePoint : nodesPoint) { final int val = XMLUtil.getElementIntValue(nodePoint, ID_VALUE, 0); setValue(ind, val); ind++; } } } else { removeAllControlPoint(); for (Node nodePoint : nodesPoint) { final int ind = XMLUtil.getElementIntValue(nodePoint, ID_INDEX, 0); final int val = XMLUtil.getElementIntValue(nodePoint, ID_VALUE, 0); setControlPoint(ind, val); } } } finally { endUpdate(); } return true; } @Override public boolean saveToXML(Node node) { if (node == null) return false; XMLUtil.setAttributeBooleanValue((Element) node, ID_RAWDATA, rawData); XMLUtil.removeChildren(node, ID_POINT); boolean result = true; if (rawData) { XMLUtil.removeChildren(node, ID_VALUE); XMLUtil.setElementBytesValue(node, ID_VALUE, asByteArray()); } else { for (int ind = 0; ind < controlPoints.size(); ind++) { final ControlPoint cp = controlPoints.get(ind); final Node nodePoint = XMLUtil.addElement(node, ID_POINT); result = result && cp.saveToXML(nodePoint); } } return result; } @Override public boolean equals(Object obj) { if (obj instanceof IcyColorMapComponent) // just compare the map content (we don't care about control point here) return Arrays.equals(map, ((IcyColorMapComponent) obj).map); return super.equals(obj); } @Override public int hashCode() { return map.hashCode(); } }