// // BaseColorControl.java // /* VisAD system for interactive analysis and visualization of numerical data. Copyright (C) 1996 - 2017 Bill Hibbard, Curtis Rueden, Tom Rink, Dave Glowacki, Steve Emmerson, Tom Whittaker, Don Murray, and Tommy Jasmin. This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; if not, write to the Free Software Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA */ package visad; import java.rmi.*; import java.util.StringTokenizer; import visad.browser.Convert; import visad.util.Util; /** BaseColorControl is the VisAD class for controlling N-component Color DisplayRealType-s.<P> */ public class BaseColorControl extends Control { /** The index of the color red */ public static final int RED = 0; /** The index of the color green */ public static final int GREEN = 1; /** The index of the color blue */ public static final int BLUE = 2; /** * The index of the alpha channel. * <P> * <B>NOTE:</B> ALPHA will always be the last index. */ public static final int ALPHA = 3; /** The default number of colors */ public final static int DEFAULT_NUMBER_OF_COLORS = 256; // color map represented by either table or function private float[][] table; private int tableLength; // = table[0].length - 1 private Function function; private transient RealTupleType functionDomainType; private transient CoordinateSystem functionCoordinateSystem; private transient Unit[] functionUnits; private transient Object lock = new Object(); private final int components; /** * Create a basic color control. * * @param d The display with which this control is associated. * @param components Either 3 (if this is a red/green/blue control) * or 4 (if there is also an alpha component). */ public BaseColorControl(DisplayImpl d, int components) { super(d); // constrain number of components to known range if (components < 3) { components = 3; } else if (components > 4) { components = 4; } this.components = components; tableLength = DEFAULT_NUMBER_OF_COLORS; table = initTableVis5D(new float[components][tableLength]); } /** * Initialize table to a grey wedge. * * @param table Table to be initialized. * * @return the initialized table. */ public static float[][] initTableGreyWedge(float[][] table) { return initTableGreyWedge(table, false); } public static float[][] initTableGreyWedge(float[][] table, boolean invert) { if (table == null || table[0] == null) { return null; } boolean hasAlpha = table.length > 3; final int numColors = table[0].length; float scale = (float) (1.0f / (float) (numColors - 1)); for (int i=0; i<numColors; i++) { int idx = invert ? (numColors-1)-i : i; table[RED][idx] = scale * i; table[GREEN][idx] = scale * i; table[BLUE][idx] = scale * i; if (hasAlpha) { table[ALPHA][idx] = scale * i; } } return table; } /** * Initialize the colormap to a grey wedge */ public void initGreyWedge() { initTableGreyWedge(table); } public void initGreyWedge(boolean invert) { initTableGreyWedge(table, invert); } /** * Initialize table to the Vis5D colormap (opaque * blue-green-red rainbow). * * @param table Table to be initialized. * * @return the initialized table. */ public static float[][] initTableVis5D(float[][] table) { if (table == null || table[0] == null) { return null; } boolean hasAlpha = table.length > 3; float curve = 1.4f; float bias = 1.0f; float rfact = 0.5f * bias; final int numColors = table[0].length; for (int i=0; i<numColors; i++) { /* compute s in [0,1] */ float s = (float) i / (float) (numColors-1); float t = curve * (s - rfact); /* t in [curve*-0.5,curve*0.5) */ table[RED][i] = (float) (0.5 + 0.5 * Math.atan( 7.0*t ) / 1.57); table[GREEN][i] = (float) (0.5 + 0.5 * (2 * Math.exp(-7*t*t) - 1)); table[BLUE][i] = (float) (0.5 + 0.5 * Math.atan( -7.0*t ) / 1.57); if (hasAlpha) { table[ALPHA][i] = 1.0f; } } return table; } /** * Initialize the colormap to the VisAD sine waves */ public void initVis5D() { initTableVis5D(table); } /** * Initialize table to the Hue-Saturation-Value colormap. * * @param table Table to be initialized. * * @return the initialized table. */ public static float[][] initTableHSV(float[][] table) { if (table == null || table[0] == null) { return null; } boolean hasAlpha = table.length > 3; float s = 1; float v = 1; final int numColors = table[0].length; for (int i=0; i<numColors; i++) { float h = i * 6 / (float )(numColors - 1); int hFloor = (int )Math.floor(h); float hPart = h - hFloor; // if hFloor is even if ((hFloor & 1) == 0) { hPart = 1 - hPart; } float m = v * (1 - s); float n = v * (1 - s*hPart); switch (hFloor) { case 0: case 6: table[RED][i] = v; table[GREEN][i] = n; table[BLUE][i] = m; break; case 1: table[RED][i] = n; table[GREEN][i] = v; table[BLUE][i] = m; break; case 2: table[RED][i] = m; table[GREEN][i] = v; table[BLUE][i] = n; break; case 3: table[RED][i] = m; table[GREEN][i] = n; table[BLUE][i] = v; break; case 4: table[RED][i] = n; table[GREEN][i] = m; table[BLUE][i] = v; break; case 5: table[RED][i] = v; table[GREEN][i] = m; table[BLUE][i] = n; break; } if (hasAlpha) { table[ALPHA][i] = 1.0f; } } return table; } /** * Initialize the colormap to Hue-Saturation-Value */ public void initHSV() { initTableHSV(table); } /** * Get the number of components of the range. * * @return Either 3 or 4 */ public int getNumberOfComponents() { return components; } /** * Get the number of colors in the table. * * @return The number of colors in the colormap. */ public int getNumberOfColors() { return tableLength; } /** * Define the color lookup by a <CODE>Function</CODE>, whose * <CODE>MathType</CODE> must have a 1-D domain and a 3-D or * 4-D <CODE>RealTupleType</CODE> range; the domain and range * <CODE>Real</CODE>s must vary over the range (0.0, 1.0) * * @param func The new <CODE>Function</CODE>. * * @exception RemoteException If there was an RMI-related problem. * @exception VisADException If there was a problem with the function. */ public void setFunction(Function func) throws RemoteException, VisADException { FunctionType baseType; if (components == 4) { baseType = FunctionType.REAL_1TO4_FUNCTION; } else { baseType = FunctionType.REAL_1TO3_FUNCTION; } if (func == null || !func.getType().equalsExceptName(baseType)) { throw new DisplayException("BaseColorControl.setFunction: " + "function must be 1D-to-" + components + "D"); } synchronized (lock) { function = func; functionDomainType = ((FunctionType) function.getType()).getDomain(); functionCoordinateSystem = function.getDomainCoordinateSystem(); functionUnits = function.getDomainUnits(); table = null; } changeControl(true); } /** * Return the color lookup <CODE>Function</CODE>. * * @return The function which defines this object's colors. */ public Function getFunction() { return function; } /** * Define the color lookup by an array of <CODE>float</CODE>s * which must have the form <CODE>float[components][table_length]</CODE>; * values should be in the range (0.0, 1.0) * * @param t The new table of colors. * * @exception RemoteException If there was a problem changing the control. * @exception VisADException If there is a problem with the table. */ public void setTable(float[][] t) throws RemoteException, VisADException { if (t == null || t[0] == null) { throw new DisplayException(getClass().getName() + ".setTable: " + "Null table"); } if (t.length != components) { if (t[0].length == components) { throw new DisplayException(getClass().getName() + ".setTable: " + " Table may be inverted"); } throw new DisplayException(getClass().getName() + ".setTable: " + "Unusable table [" + t.length + "][" + t[0].length + "], expected [" + components + "][]"); } if (t[RED] == null || t[GREEN] == null || t[BLUE] == null || (t.length > ALPHA && t[ALPHA] == null)) { throw new DisplayException(getClass().getName() + ".setTable: " + "One or more component lists is null"); } if (t[RED].length != t[GREEN].length || t[RED].length != t[BLUE].length || (components > ALPHA && t[RED].length != t[ALPHA].length)) { throw new DisplayException("BaseColorControl.setTable: " + "Inconsistent table lengths"); } synchronized (lock) { tableLength = t[0].length; table = new float[components][tableLength]; for (int j=0; j<components; j++) { System.arraycopy(t[j], 0, table[j], 0, tableLength); } function = null; } changeControl(true); } /** * Get the table of colors. * * @return The color table. */ public float[][] getTable() { if (table == null) return null; float[][] t = new float[components][tableLength]; for (int j=0; j<components; j++) { System.arraycopy(table[j], 0, t[j], 0, tableLength); } return t; } /** * If the colors are defined using a color table, get a * <CODE>String</CODE> that can be used to reconstruct this * object later. If the colors are defined using a * <CODE>Function</CODE>, return null. * * @return The save string describing this object. */ public String getSaveString() { if (table == null) return null; int len = table.length; int len0 = table[0].length; StringBuffer sb = new StringBuffer(15 * len * len0); sb.append(len); sb.append(" x "); sb.append(len0); sb.append('\n'); for (int j=0; j<len0; j++) { sb.append(table[RED][j]); for (int i=1; i<len; i++) { sb.append(' '); sb.append(table[i][j]); } sb.append('\n'); } return sb.toString(); } /** * Reconstruct this control using the specified save string. * * @param save The save string. * * @exception VisADException If the save string is not valid. * @exception RemoteException If there was a problem setting the table. */ public void setSaveString(String save) throws RemoteException, VisADException { if (save == null) throw new VisADException("Invalid save string"); StringTokenizer st = new StringTokenizer(save); int numTokens = st.countTokens(); if (numTokens < 3) throw new VisADException("Invalid save string"); // get table size int len = Convert.getInt(st.nextToken()); if (len < 1) { throw new VisADException("First dimension is not positive"); } if (!st.nextToken().equalsIgnoreCase("x")) { throw new VisADException("Invalid save string"); } int len0 = Convert.getInt(st.nextToken()); if (len0 < 1) { throw new VisADException("Second dimension is not positive"); } if (numTokens < 3 + len * len0) { throw new VisADException("Not enough table entries"); } // get table entries float[][] t = new float[len][len0]; for (int j=0; j<len0; j++) { for (int i=0; i<len; i++) t[i][j] = Convert.getFloat(st.nextToken()); } setTable(t); } /** * Return a list of colors for specified values. * * @param values The values to look up. It is expected that * they nominally lie in the range of 0 through 1. * Values outside this range will be assigned the * color of the nearest end. NaN values will be * assigned NaN colors. * @return The list of colors. Element <code>[i][j]</code> * is the value of the <code>i</code>-th color * component for <code>values[j]</code>, where * <code>i</code> is {@link #RED}, {@link #GREEN}, * {@link #BLUE}, or {@link #ALPHA}. A component * value is in the range from 0 through 1, or is * NaN. * @throws RemoteException If there was an RMI-related problem. * @throws VisADException If the function encountered a problem. */ public float[][] lookupValues(float[] values) throws RemoteException, VisADException { if (values == null) { return null; } final int tblEnd = tableLength - 1; final int valLen = values.length; float[][] colors = null; synchronized (lock) { if (table != null) { colors = new float[components][valLen]; float scale = (float) tableLength; try { for (int i=0; i<valLen; i++) { if (values[i] != values[i]) { colors[RED][i] = Float.NaN; colors[GREEN][i] = Float.NaN; colors[BLUE][i] = Float.NaN; if (components > ALPHA) { colors[ALPHA][i] = Float.NaN; } } else { int j = (int) (scale * values[i]); // note actual table length is tableLength + 1 // extend first and last table entries to 'infinity' if (j < 0) { colors[RED][i] = table[RED][0]; colors[GREEN][i] = table[GREEN][0]; colors[BLUE][i] = table[BLUE][0]; if (components > ALPHA) { colors[ALPHA][i] = table[ALPHA][0]; } } else if (tableLength <= j) { colors[RED][i] = table[RED][tblEnd]; colors[GREEN][i] = table[GREEN][tblEnd]; colors[BLUE][i] = table[BLUE][tblEnd]; if (components > ALPHA) { colors[ALPHA][i] = table[ALPHA][tblEnd]; } } else { colors[RED][i] = table[RED][j]; colors[GREEN][i] = table[GREEN][j]; colors[BLUE][i] = table[BLUE][j]; if (components > ALPHA) { colors[ALPHA][i] = table[ALPHA][j]; } } } } // end for (int i=0; i<valLen; i++) } catch (ArrayIndexOutOfBoundsException e) { } } else if (function != null) { List1DSet set = new List1DSet(values, functionDomainType, functionCoordinateSystem, functionUnits); Field field = function.resample(set, Data.WEIGHTED_AVERAGE, Data.NO_ERRORS); colors = Set.doubleToFloat(field.getValues()); } } return colors; } /** * Return a list of colors for the specified range. */ public float[][] lookupRange(int left, int right) throws VisADException, RemoteException { if (left < 0 || right >= tableLength || left > right) { throw new VisADException("Bad left/right value"); } final int tblEnd = tableLength - 1; final int valLen = (right - left) + 1; float[][] colors = null; synchronized (lock) { if (table != null) { colors = new float[components][valLen]; for (int i=0; i<valLen; i++) { colors[RED][i] = table[RED][i+left]; colors[GREEN][i] = table[GREEN][i+left]; colors[BLUE][i] = table[BLUE][i+left]; if (components > ALPHA) { colors[ALPHA][i] = table[ALPHA][i+left]; } } } else if (function != null) { double scale = (double) tableLength; Linear1DSet set = new Linear1DSet(functionDomainType, (double ) (left / scale), (double ) (right / scale), valLen, functionCoordinateSystem, functionUnits, null); Field field = function.resample(set, Data.NEAREST_NEIGHBOR, Data.NO_ERRORS); colors = Set.doubleToFloat(field.getValues()); } } return colors; } /** * Set the specified range to the specified colors. */ public void setRange(int left, int right, float[][] colors) throws VisADException, RemoteException { if (left < 0 || right >= tableLength || left > right) { throw new VisADException("Bad left/right value"); } if (colors == null || colors.length != components || colors[RED] == null || colors[GREEN] == null || colors[BLUE] == null || (colors.length > ALPHA && colors[ALPHA] == null)) { throw new VisADException("Bad range table!"); } if (table == null) { throw new VisADException("Cannot set values for function!"); } final int valLen = (right - left) + 1; if (colors[RED].length != valLen || colors[GREEN].length != valLen || colors[BLUE].length != valLen || (colors.length > ALPHA && colors[ALPHA].length != valLen)) { throw new VisADException("Array does not contain " + valLen + " colors!"); } synchronized (lock) { for (int i=0; i<valLen; i++) { table[RED][i+left] = colors[RED][i]; table[GREEN][i+left] = colors[GREEN][i]; table[BLUE][i+left] = colors[BLUE][i]; if (components > ALPHA) { table[ALPHA][i+left] = colors[ALPHA][i]; } } } changeControl(true); } /** * Compare the specified table to this object's table. * * @param newTable Table to compare. * * @return <CODE>true</CODE> if <CODE>newTable</CODE> is the * same as this object's table. */ private boolean tableEquals(float[][] newTable) { if (table == null) { if (newTable != null) { return false; } } else if (newTable == null) { return false; } else if (table != newTable) { if (table.length != newTable.length) { return false; } else { int i; for (i = 0; i < table.length; i++) { if (table[i].length != newTable[i].length) { return false; } } for (i = 0; i < table.length; i++) { for (int j = 0; j < table[i].length; j++) { if (!Util.isApproximatelyEqual(table[i][j], newTable[i][j])) { return false; } } } } } return true; } /** * Compare the specified function to this object's function. * * @param newFunc Function to compare. * * @return <CODE>true</CODE> if <CODE>newFunc</CODE> is the * same as this object's function. */ private boolean functionEquals(Function newFunc) { if (function == null) { if (newFunc != null) { return false; } } else if (newFunc == null) { return false; } else if (!function.equals(newFunc)) { return false; } return true; } /** * Copy the state of a remote control to this control. * * @param rmt The control to be copied. * * @exception VisADException If the remote control cannot be copied. */ public void syncControl(Control rmt) throws VisADException { if (rmt == null) { throw new VisADException("Cannot synchronize " + getClass().getName() + " with null Control object"); } if (!(rmt instanceof BaseColorControl)) { throw new VisADException("Cannot synchronize " + getClass().getName() + " with " + rmt.getClass().getName()); } BaseColorControl bcc = (BaseColorControl )rmt; boolean changed = false; boolean tableChanged = !tableEquals(bcc.table); boolean functionChanged = !functionEquals(bcc.function); if (tableChanged) { if (bcc.table == null) { if (functionChanged ? bcc.function == null : function == null) { throw new VisADException("BaseColorControl has null Table," + " but no Function"); } table = null; } else { if (bcc.table.length != components) { throw new VisADException("Table must be float[" + components + "][], not float[" + bcc.table.length + "][]"); } synchronized (lock) { tableLength = bcc.table[0].length; for (int i = 0; i < components; i++) { if (table[i].length != bcc.table[i].length) { table[i] = new float[bcc.table[i].length]; } System.arraycopy(bcc.table[i], 0, table[i], 0, bcc.table[i].length); } tableLength = table[0].length; function = null; } try { changeControl(true); } catch (RemoteException re) { throw new VisADException("Could not indicate that control" + " changed: " + re.getMessage()); } } } if (functionChanged) { if (bcc.function == null) { if (table == null) { throw new VisADException("ColorControl has null Function," + " but no Table"); } function = null; } else { try { setFunction(bcc.function); } catch (RemoteException re) { throw new VisADException("Could not set function: " + re.getMessage()); } } } } /** * Return <CODE>true</CODE> if this object is "equal" to the parameter. * * @param o Object to compare. * * @return <CODE>true</CODE> if this object "equals" <CODE>o</CODE>. */ public boolean equals(Object o) { if (!super.equals(o)) { return false; } BaseColorControl bcc = (BaseColorControl )o; if (tableLength != bcc.tableLength) { return false; } if (!tableEquals(bcc.table)) { return false; } if (!functionEquals(bcc.function)) { return false; } return true; } public Object clone() { BaseColorControl bcc = (BaseColorControl )super.clone(); if (table != null) { bcc.table = new float[table.length][]; for (int i = table.length - 1; i >= 0; i--) { bcc.table[i] = (float[] )table[i].clone(); } } return bcc; } private static char dirChar(int down, int same, int up) { char ch; if (down == 0 || same == 0 || up == 0) { if (down > 0) { if (up > 0) { if (down > up) { return 'v'; } return '^'; } if (same > 0) { return '~'; } return '\\'; } else if (up > 0) { if (same > 0) { return '~'; } return '/'; } else { return '_'; } } if (down > same) { if (down > (same + up)) { return '\\'; } if (up > (down + same)) { return '/'; } if (up > same) { return '^'; } } if (up > same) { if (up > (down + same)) { return '/'; } if (down > (same + up)) { return '\\'; } } if (same > (down + up)) { return '-'; } return '~'; } public String toString() { int binLen = tableLength; int binSize = 1; while (binLen > 32) { binLen >>= 1; binSize <<= 1; } String className = getClass().getName(); int dot = className.lastIndexOf('.'); if (dot >= 0) { className = className.substring(dot+1); } StringBuffer buf = new StringBuffer(className); buf.append('['); String colorInitial = "RGBA"; for (int c = 0; c < components; c++) { if (c > 0) { buf.append(','); } buf.append(colorInitial.charAt(c)); buf.append('='); float prev = table[c][0]; int tot = 0; while (tot < tableLength) { int trendDown, trendSame, trendUp; trendDown = trendSame = trendUp = 0; for (int i = 0; i < binSize; i++) { float curr = table[c][tot+i]; if (Math.abs(curr - prev) <= 0.0001) { trendSame++; } else if (curr < prev) { trendDown++; } else { trendUp++; } prev = curr; } buf.append(dirChar(trendDown, trendSame, trendUp)); tot += binSize; } } buf.append(']'); return buf.toString(); } }