/* @(#) $Id: BaseRGBMap.java,v 1.22 2002-09-19 21:08:42 curtis Exp $ VisAD Utility Library: Widgets for use in building applications with the VisAD interactive analysis and visualization library Copyright (C) 2017 Nick Rasmussen VisAD is Copyright (C) 1996 - 2017 Bill Hibbard, Curtis Rueden, Tom Rink and Dave Glowacki. This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 1, or (at your option) any later version. This program 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 General Public License in file NOTICE for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. */ package visad.util; import java.awt.Color; import java.awt.Cursor; import java.awt.Dimension; import java.awt.Graphics; import java.awt.Point; import java.awt.Toolkit; import java.awt.event.MouseEvent; import java.awt.event.MouseListener; import java.awt.event.MouseMotionListener; import java.rmi.RemoteException; import visad.BaseColorControl; import visad.ControlEvent; import visad.ControlListener; import visad.VisADException; /** * An extensible RGB colormap with no interpolation between the * internally stored values. Click and drag with the left mouse * button to draw the color curves. Click with the right or middle * mouse button to alternate between the color curves. * * @author Nick Rasmussen nick@cae.wisc.edu * @version $Revision: 1.22 $, $Date: 2002-09-19 21:08:42 $ * @since Visad Utility Library, 0.5 */ public class BaseRGBMap extends ColorMap implements ControlListener, MouseListener, MouseMotionListener { /** change this to <TT>true</TT> to use color cursors */ public static boolean USE_COLOR_CURSORS = false; /** default resolution */ public static final int DEFAULT_RESOLUTION = BaseColorControl.DEFAULT_NUMBER_OF_COLORS; /** The color control */ private BaseColorControl ctl; /** The left modified value */ private int valLeft = 0; /** The right modified value */ private int valRight = 0; /** A lock to synchronize against when modifying the modified area */ private Object mutex = new Object(); /** The index of the color red */ private static final int RED = BaseColorControl.RED; /** The index of the color green */ private static final int GREEN = BaseColorControl.GREEN; /** The index of the color blue */ private static final int BLUE = BaseColorControl.BLUE; /** The index of the alpha channel */ private static final int ALPHA = BaseColorControl.ALPHA; /** The current color for the mouse to draw on */ private int state = RED; /** The resolution of the map */ private int resolution = 256; /** 'true' if this map has an alpha component */ private boolean hasAlpha; private static Cursor[] cursor = null; /** a slightly brighter blue */ private static final Color bluish = new Color(80, 80, 255); /** the preferred size of this widget */ private Dimension preferredSize = null; /** * Construct a BaseRGBMap with the default resolution * * @param hasAlpha set to <TT>true</TT> is this map has * an alpha component */ public BaseRGBMap(boolean hasAlpha) throws RemoteException, VisADException { this(defaultTable(DEFAULT_RESOLUTION, hasAlpha)); } /** * Construct a colormap with the specified resolution * * @param resolution the length of the array * @param hasAlpha set to <TT>true</TT> is this map has * an alpha component */ public BaseRGBMap(int resolution, boolean hasAlpha) throws RemoteException, VisADException { this(defaultTable(resolution, hasAlpha)); } /** * Construct a colormap initialized with the supplied tuples * * @param vals the tuples used to initialize the colormap * @param hasAlpha <TT>true</TT> if the colormap should * have an ALPHA component. * * @deprecated <TT>hasAlpha</TT> isn't really necessary. */ public BaseRGBMap(float[][] vals, boolean hasAlpha) throws RemoteException, VisADException { this(vals != null ? vals : defaultTable(DEFAULT_RESOLUTION, hasAlpha)); } /** * Construct a colormap initialized with the supplied tuples * * @param vals the tuples used to initialize the colormap * See setValues() for constraints on the <TT>vals</TT> array. */ public BaseRGBMap(float[][] vals) throws RemoteException, VisADException { if (vals == null) { vals = defaultTable(DEFAULT_RESOLUTION, true); } setValues(vals); if (USE_COLOR_CURSORS) buildCursors(); addMouseListener(this); addMouseMotionListener(this); ctl.addControlListener(this); } /** * Construct a colormap from the specified control. * * @param ctl control to use as data source */ public BaseRGBMap(BaseColorControl ctl) { this.ctl = ctl; hasAlpha = (ctl.getNumberOfComponents() == 4); resolution = ctl.getNumberOfColors(); if (USE_COLOR_CURSORS) buildCursors(); addMouseListener(this); addMouseMotionListener(this); ctl.addControlListener(this); } /** * Build a table with the given number of components and colors * * @param resolution Number of colors. * @param hasAlpha <TT>true</TT> if this colormap has an alpha component. * * @return The new table. */ static float[][] defaultTable(int resolution, boolean hasAlpha) { final int components = hasAlpha ? 4 : 3; float[][] tbl = new float[components][resolution]; return BaseColorControl.initTableVis5D(tbl); } /** * Build one of the red, green, blue and alpha cursors * * @param rgba cursor to build (RED, GREEN, BLUE or ALPHA) * * @return the new <TT>Cursor</TT> */ static Cursor buildRGBACursor(int rgba) { if (rgba < 0 || rgba > 3) rgba = 0; final int lines = 15; final int elements = 15; int[] pixel = new int[lines*elements]; for (int i = 0; i < pixel.length; i++) { pixel[i] = 0; } final int color; switch (rgba) { case RED: color = Color.red.getRGB(); break; case GREEN: color = Color.green.getRGB(); break; case BLUE: color = bluish.getRGB(); break; default: case ALPHA: color = Color.gray.getRGB(); break; } final int midLine = (lines / 2) * elements; for (int i = midLine + elements - 1; i >= midLine; i--) { pixel[i] = color; } final int midElement = (elements / 2); for (int i = 0; i < lines; i++) { pixel[i*elements + midElement] = color; } java.awt.image.ImageProducer ip; ip = new java.awt.image.MemoryImageSource(elements, lines, pixel, 0, elements); java.awt.Image img = Toolkit.getDefaultToolkit().createImage(ip); Point pt = new Point(img.getWidth(null) / 2, img.getHeight(null) / 2); String name; switch (rgba) { case RED: name = "crossRed"; break; case GREEN: name = "crossGreen"; break; case BLUE: name = "crossBlue"; break; default: case ALPHA: name = "crossAlpha"; break; } return Toolkit.getDefaultToolkit().createCustomCursor(img, pt, name); } /** * Used internally to initialize the red, green, blue and alpha cursors */ private void buildCursors() { if (cursor != null) return; // only try to change the cursor if we're running under JDK 1.3 or greater String jVersion = System.getProperty("java.version"); if (jVersion == null) return; if (jVersion.length() < 3) return; if (jVersion.charAt(0) < '1') return; if (jVersion.charAt(1) != '.') return; if (jVersion.charAt(0) == '1' && jVersion.charAt(2) < '3') return; cursor = new Cursor[4]; cursor[RED] = buildRGBACursor(RED); cursor[GREEN] = buildRGBACursor(GREEN); cursor[BLUE] = buildRGBACursor(BLUE); cursor[ALPHA] = buildRGBACursor(ALPHA); setCursor(cursor[state]); } /** * Sets the values of the internal array after the map * has been created. * * The table should be <TT>float[resolution][dimension]</TT> * where <TT>dimension</TT> is either 3 (for an RGB table) * or 4 (if the table also has an alpha component) and * <TT>resolution</TT> is the number of colors in the table, * which must be greater than 4. * * @param newVal the color tuples used to initialize the map. */ public void setValues(float[][] newVal) throws RemoteException, VisADException { if (newVal == null) { throw new VisADException("Can't set table to null"); } if (newVal.length >= 3 && newVal.length <= 4 && newVal[0].length > 4) { hasAlpha = newVal.length > 3; resolution = newVal[0].length; } else if (newVal[0].length >= 3 && newVal[0].length <= 4 && newVal.length > 4) { // table is inverted hasAlpha = newVal[0].length > 3; resolution = newVal.length; float[][] tmpVal = new float[hasAlpha ? 4 : 3][resolution]; for (int i = 0; i < resolution; i++) { tmpVal[RED][i] = newVal[i][RED]; tmpVal[GREEN][i] = newVal[i][GREEN]; tmpVal[BLUE][i] = newVal[i][BLUE]; if (hasAlpha) { tmpVal[ALPHA][i] = newVal[i][ALPHA]; } } newVal = tmpVal; } else { throw new VisADException("Cannot set table with dimensions [" + newVal.length + "][" + newVal[0].length + "]"); } if (ctl == null) { ctl = new BaseColorControl(null, hasAlpha ? 4 : 3); } ctl.setTable(newVal); sendUpdate(0, resolution-1); } /** * Get the resolution of the map * * @return the number of colors in the map. */ public int getMapResolution() { return resolution; } /** * Get the dimension of the map * * @return either 3 or 4 */ public int getMapDimension() { return ctl.getNumberOfComponents(); } /** * Get the color map (as an array of <TT>float</TT> tuples. * * @return a copy of the color map */ public float[][] getColorMap() { return ctl.getTable(); } /** * Returns the tuple at a floating point value val * * @param firstVal the location to start. * @param lastVal the location to finish. * @param num the number of tuples to return. * * @return The array of 3 or 4 element arrays. */ public float[][] getTuples(float firstVal, float lastVal, int num) { if (num <= 0 || firstVal > lastVal || (num == 1 && !Util.isApproximatelyEqual(firstVal, lastVal))) { return null; } float floatIdx; floatIdx = firstVal * (resolution - 1); int startIndex = (int )Math.floor(floatIdx); floatIdx = lastVal * (resolution - 1); int endIndex = (int )Math.floor(floatIdx); float partialEnd = floatIdx - endIndex; boolean isPartialEnd = (partialEnd != 0); float[][] tuples = new float[num][hasAlpha ? 4 : 3]; int rStart = startIndex; if (rStart < 0) { rStart = 0; } else if (rStart >= resolution) { rStart = resolution - 1; } int rEnd = (isPartialEnd ? endIndex+1 : endIndex); if (rEnd >= resolution) { rEnd = resolution - 1; } else if (rEnd < 0) { rEnd = 0; } final int rLen = (rEnd - rStart) + 1; float[][] colors; try { colors = ctl.lookupRange(rStart, rEnd); } catch (Exception e) { System.err.println("Error in " + getClass().getName() + ": " + e.getClass().getName() + ": " + e.getMessage()); return null; } float stepVal = (lastVal - firstVal) / (float )num; for (int i = 0; i < num; i++) { float thisVal = firstVal + (float )i * stepVal; floatIdx = thisVal * (resolution - 1); int index = (int )Math.floor(floatIdx); float partial = floatIdx - index; boolean isPartial = (partial != 0); index -= rStart; if (index < 0 || index >= rLen || (index == (rLen - 1) && isPartial)) { tuples[i][RED] = tuples[i][GREEN] = tuples[i][BLUE] = 0; if (hasAlpha) { tuples[i][ALPHA] = 0; } continue; } if (isPartial) { tuples[i][RED] = colors[RED][index] * (1 - partial) + colors[RED][index+1] * partial; tuples[i][GREEN] = colors[GREEN][index] * (1 - partial) + colors[GREEN][index+1] * partial; tuples[i][BLUE] = colors[BLUE][index] * (1 - partial) + colors[BLUE][index+1] * partial; if (hasAlpha) { tuples[i][ALPHA] = colors[ALPHA][index] * (1 - partial) + colors[ALPHA][index+1] * partial; } } else { tuples[i][RED] = colors[RED][index]; tuples[i][GREEN] = colors[GREEN][index]; tuples[i][BLUE] = colors[BLUE][index]; if (hasAlpha) { tuples[i][ALPHA] = colors[ALPHA][index]; } } } return tuples; } /** * Returns the tuple at a floating point value val * * <B>WARNING</B>: This is a <I>really</I> slow way to * get a color, so don't use it inside a loop. * * @param value the location to return. * * @return The 3 or 4 element array. */ public float[] getTuple(float value) { float[][] v = getTuples(value, value, 1); return (v == null ? null : v[0]); } /** * Implementation of the abstract function in ColorMap * * <B>WARNING</B>: This is a <I>really</I> slow way to * get a color, so don't use it inside a loop. */ public float[][] getRGBTuples(float startVal, float endVal, int num) { float[][] t = getTuples(startVal, endVal, num); if (t[0].length > 3) { float[][] f = new float[t.length][3]; for (int i = t.length - 1; i >= 0; i--) { f[i][RED] = t[i][RED]; f[i][GREEN] = t[i][GREEN]; f[i][BLUE] = t[i][BLUE]; } t = f; } return t; } /** * Implementation of the abstract function in ColorMap * * <B>WARNING</B>: This is a <I>really</I> slow way to * get a color, so don't use it inside a loop. * * @param value a floating point number between 0 and 1 * @return an RGB tuple of floating point numbers in the * range 0 to 1 */ public float[] getRGBTuple(float value) { float[] t = getTuple(value); if (t.length > 3) { float[] f = new float[3]; f[RED] = t[RED]; f[GREEN] = t[GREEN]; f[BLUE] = t[BLUE]; t = f; } return t; } /** * Redraw the between the <TT>left</TT> and * <TT>right</TT> colors * * @param left the left edge of the changed area (in the range 0.0-1.0) * @param right the right edge of the changed area */ protected void sendUpdate(int left, int right) { notifyListeners(new ColorChangeEvent(this, left, right)); if (left != 0) { left--; } if (right != resolution - 1) { right++; } synchronized (mutex) { if (left < valLeft) valLeft = left; if (right > valRight) valRight = right; } // redraw validate(); repaint(); } /** * Present to implement MouseListener, currently ignored * * @param evt ignored */ public void mouseClicked(MouseEvent evt) { } /** * MouseListener, currently ignored * * @param evt ignored */ public void mouseEntered(MouseEvent evt) { } /** * MouseListener method, currently ignored * * @param evt ignored */ public void mouseExited(MouseEvent evt) { } /** The last mouse event's x value */ private int oldX; /** The last mouse event's y value */ private int oldY; /** A synchronization primitive for the mouse movements */ private Object mouseMutex = new Object(); /** * Updates the associated Control * * @param evt the mouse press event */ public void mousePressed(MouseEvent evt) { if ((evt.getModifiers() & evt.BUTTON1_MASK) == 0 && evt.getModifiers() != 0) { return; } int width = getBounds().width; int height = getBounds().height; int x = evt.getX(); int y = evt.getY(); if (x < 0) x = 0; else if (x >= width) x = width - 1; if (y < 0) y = 0; else if (y >= height) y = height - 1; float step = (float )(resolution - 1) / (float )width; int pos = (int )Math.floor((float )x * step + 0.5); float[][] colors; try { colors = ctl.lookupRange(pos, pos); } catch (Exception e) { System.err.println("Error in " + getClass().getName() + ": " + e.getClass().getName() + ": " + e.getMessage()); return; } colors[state][0] = 1 - (float )y / (float )height; try { ctl.setRange(pos, pos, colors); } catch (Exception e) { System.err.println("Error in " + getClass().getName() + ": " + e.getClass().getName() + ": " + e.getMessage()); return; } oldX = x; oldY = y; sendUpdate(pos, pos); } /** * Listens for releases of the right mouse button, * and changes the active color * * @param evt the release event */ public void mouseReleased(MouseEvent evt) { if ((evt.getModifiers() & (evt.BUTTON2_MASK|evt.BUTTON3_MASK)) == 0) { return; } state = (state + 1) % (hasAlpha ? 4 : 3); if (cursor != null) { setCursor(cursor[state]); } } /** * Updates the associated Control * * @param evt the drag event */ public void mouseDragged(MouseEvent evt) { if ((evt.getModifiers() & evt.BUTTON1_MASK) == 0 && evt.getModifiers() != 0) { return; } drag(evt.getX(), evt.getY(), oldX, oldY); oldX = evt.getX(); oldY = evt.getY(); } /** * Internal mouse dragging function * * @param x the current x coordinate * @param y the current y coordinate * @param oldx the starting x coordinate * @param oldy the starting y coordinate */ private void drag(int x, int y, int oldx, int oldy) { int width = getBounds().width; int height = getBounds().height; // make sure x, y, oldx and oldy are all inside the window if (x < 0) x = 0; else if (x >= width) x = width - 1; if (y < 0) y = 0; else if (y >= height) y = height - 1; if (oldx < 0) oldx = 0; else if (oldx >= width) oldx = width - 1; if (oldy < 0) oldy = 0; else if (oldy >= height) oldy = height - 1; float step = (float )(resolution - 1) / (float )width; int oldPos = (int )Math.floor((float )oldx * step + 0.5); int newPos = (int )Math.floor((float )x * step + 0.5); float oldVal = 1 - (float )oldy / (float )height; float newVal = 1 - (float )y / (float )height; final int start, finish; final int len = ctl.getNumberOfColors() - 1; if (newPos > oldPos) { start = oldx < width - 1 ? oldPos : len; finish = x < width - 1 ? newPos : len; } else { start = x < width - 1 ? newPos : len; finish = oldx < width - 1 ? oldPos : len; } float[][] colors; try { colors = ctl.lookupRange(start, finish); } catch (Exception e) { System.err.println("Error in " + getClass().getName() + ": " + e.getClass().getName() + ": " + e.getMessage()); return; } final float loVal, hiVal; if (newPos > oldPos) { loVal = newVal; hiVal = oldVal; } else { loVal = oldVal; hiVal = newVal; } final int total = finish - start + 1; for (int i = 0; i < total; i++) { float v = ((hiVal * (float )(total - i) + loVal * (float )i) / (float )total); colors[state][i] = v; } try { ctl.setRange(start, finish, colors); } catch (Exception e) { System.err.println("Error in " + getClass().getName() + ": " + e.getClass().getName() + ": " + e.getMessage()); return; } sendUpdate(start, finish); } /** * MouseMovementListener method, currently ignored * * @param evt ignored */ public void mouseMoved(MouseEvent evt) { } /** * Repaints the entire JPanel * * @param g The <TT>Graphics</TT> to update. */ public void paint(Graphics g) { synchronized (mutex) { valLeft = 0; valRight = resolution - 1; } update(g); } /** The left bound for updating the JPanel */ private float updateLeft = 0; /** The right bound for updating the JPanel */ private float updateRight = 1; /** * Repaints the modified areas of the JPanel * * @param g The <TT>Graphics</TT> to update. */ public void update(Graphics g) { final int maxRight = resolution - 1; int left = 0; int right = maxRight; synchronized (mutex) { if (valLeft > valRight) { return; } left = valLeft; right = valRight; valLeft = maxRight; valRight = 0; } final int numColors = ctl.getNumberOfColors() - 1; if (left < 0) { left = 0; } else if (left > numColors) { left = numColors; } if (right < 0) { right = 0; } else if (right > numColors) { right = numColors; } if (left > 0) { left--; } if (right < maxRight) { right++; } final int maxWidth = getBounds().width - 1; final int maxHeight = getBounds().height - 1; int leftPixel = (left * maxWidth) / maxRight; int rightPixel = (right * maxWidth) / maxRight; g.setColor(Color.black); g.fillRect(leftPixel,0,rightPixel - leftPixel + 1, maxHeight + 1); if (left > 0) { left--; } if (right < maxRight) { right++; } leftPixel = (left * maxWidth) / maxRight; rightPixel = (right * maxWidth) / maxRight; float[][] colors; try { colors = ctl.lookupRange(left, right < maxRight ? right + 1 : maxRight); } catch (Exception e) { System.err.println("Error in " + getClass().getName() + ": " + e.getClass().getName() + ": " + e.getMessage()); colors = null; } if (colors == null) { return; } int prevEnd = leftPixel; int prevRed = (int )Math.floor((1 - colors[RED][0]) * maxHeight); int prevGreen = (int )Math.floor((1 - colors[GREEN][0]) * maxHeight); int prevBlue = (int )Math.floor((1 - colors[BLUE][0]) * maxHeight); int prevAlpha; if (hasAlpha) { prevAlpha = (int )Math.floor((1 - colors[ALPHA][0]) * maxHeight); } else { prevAlpha = 0; } int alpha = 0; for (int i = 1; i < colors[0].length; i++) { int lineEnd = ((left + i) * maxWidth) / maxRight; int red = (int )Math.floor((1 - colors[RED][i]) * maxHeight); int green = (int )Math.floor((1 - colors[GREEN][i]) * maxHeight); int blue = (int )Math.floor((1 - colors[BLUE][i]) * maxHeight); if (hasAlpha) { alpha = (int )Math.floor((1 - colors[ALPHA][i]) * maxHeight); } g.setColor(Color.red); g.drawLine(prevEnd, prevRed, lineEnd, red); g.setColor(Color.green); g.drawLine(prevEnd, prevGreen, lineEnd, green); g.setColor(bluish); g.drawLine(prevEnd, prevBlue, lineEnd, blue); if (hasAlpha) { g.setColor(Color.gray); g.drawLine(prevEnd, prevAlpha, lineEnd, alpha); } prevEnd = lineEnd; prevRed = red; prevGreen = green; prevBlue = blue; if (hasAlpha) { prevAlpha = alpha; } } } /** * Return the preferred size of this map, taking into account * the resolution. * * @return preferred size. */ public Dimension getPreferredSize() { if (preferredSize == null) { final int height = 170; preferredSize = new Dimension(resolution, height); } return preferredSize; } /** * Override the widget's calculation for the preferred size * of this map. * * @param pref preferred size. */ public void setPreferredSize(Dimension pref) { preferredSize = pref; } /** * If the color data in the <CODE>Control</CODE> associated with this * widget's <CODE>Control</CODE> has changed, update the data in * the <CODE>ColorMap</CODE>. * * @param evt Data from the changed <CODE>Control</CODE>. */ public void controlChanged(ControlEvent evt) throws RemoteException, VisADException { hasAlpha = (ctl.getNumberOfComponents() == 4); resolution = ctl.getNumberOfColors(); sendUpdate(0, getMapResolution()-1); } }