import java.awt.*; import javax.swing.*; import java.awt.image.BufferedImage; /** * A MosaicPanel object represents a grid containing rows * and columns of colored rectangles. There can be "grouting" * between the rectangles. (The grouting is just drawn as a * one-pixel outline around each rectangle.) The rectangles can * optionally be drawn as raised 3D-style rectangles. Methods are * provided for getting and setting the colors of the rectangles. */ public class MosaicPanel extends JPanel { //------------------ private instance variables -------------------- private int rows; // The number of rows of rectangles in the grid. private int columns; // The number of columns of rectangles in the grid. private Color defaultColor; // Color used for any rectangle whose color // has not been set explicitly. This // can never be null. private Color groutingColor; // The color for "grouting" between // rectangles. If this is null, no // grouting is drawn. private boolean alwaysDrawGrouting; // Grouting is drawn around default- // colored rects if this is true. private boolean use3D = true; // If true, 3D rects are used; if false, // flat rects are used to draw the rectangles. private boolean autopaint = true; // If true, then when a square's color is set, // repaint is called automatically. private Color[][] grid; // An array that contains the rectangles' colors. // If a null occurs in this array, the rectangle // is drawn in the default color, and "grouting" // will be drawn around that rectangle only if // alwaysDrawGrouting is true. Also, the // rectangle is drawn as a flat rectangle rather // than as a 3D rectangle. private BufferedImage OSI; // The mosaic is actually drawn here, then the image //is copied to the screen private Graphics OSG; // Graphics context for drawing to OSI private boolean needsRedraw; // This is set to true when a change has occurred that // changes the appearance of the mosaic. //------------------------ constructors ----------------------------- /** * Construct a MosaicPanel with 42 rows and 42 columns of rectangles, * and with preferred rectangle height and width both set to 16. */ public MosaicPanel() { this(42,42,16,16); } /** * Construct a MosaicPanel with specified numbers of rows and columns of rectangles, * and with preferred rectangle height and width both set to 16. */ public MosaicPanel(int rows, int columns) { this(rows,columns,16,16); } /** * Construct a MosaicPanel with the specified number of rows and * columns of rectangles, and with a specified preferred size for the * rectangle. The default rectangle color is black, the * grouting color is gray, and alwaysDrawGrouting is set to false. * @param rows the mosaic will have this many rows of rectangles. This must be a positive number. * @param columns the mosaic will have this many columns of rectangles. This must be a positive number. * @param preferredBlockWidth the preferred width of the mosaic will be set this value times the number of * columns. The actual width is set by the component that contains the mosaic, and so might not be * equal to the preferred width. Size is measured in pixels. The value should not be less than about 5. * @param preferredBlockHeight the preferred height of the mosaic will be set this value times the number of * rows. The actual height is set by the component that contains the mosaic, and so might not be * equal to the preferred height. Size is measured in pixels. The value should not be less than about 5. */ public MosaicPanel(int rows, int columns, int preferredBlockWidth, int preferredBlockHeight) { this(rows, columns, preferredBlockWidth, preferredBlockHeight, null, 0); } /** * Construct a MosaicPanel with the specified number of rows and * columns of rectangles, and with a specified preferred size for the * rectangle. The default rectangle color is black, the * grouting color is gray, and alwaysDrawGrouting is set to false. * If a non-null border color is specified, then a border of that color is added * to the panel, and its width is taken into account in the computation of the preferred * size of the panel. * @param rows the mosaic will have this many rows of rectangles. This must be a positive number. * @param columns the mosaic will have this many columns of rectangles. This must be a positive number. * @param preferredBlockWidth the preferred width of the mosaic will be set this value times the number of * columns. The actual width is set by the component that contains the mosaic, and so might not be * equal to the preferred width. Size is measured in pixels. The value should not be less than about 5. * @param preferredBlockHeight the preferred height of the mosaic will be set this value times the number of * rows. The actual height is set by the component that contains the mosaic, and so might not be * equal to the preferred height. Size is measured in pixels. The value should not be less than about 5. * @param borderColor if non-null, a border of this color is added to the panel. The border is then * taken into account in the computation of the panel's preferred size. * @param borderWidth if borderColor is non-null, then this parameter gives the width of the border; any * value less than 1 is treated as 1. */ public MosaicPanel(int rows, int columns, int preferredBlockWidth, int preferredBlockHeight, Color borderColor, int borderWidth) { this.rows = rows; this.columns = columns; grid = new Color[rows][columns]; defaultColor = Color.BLACK; groutingColor = Color.GRAY; alwaysDrawGrouting = false; setBackground(defaultColor); setOpaque(true); setDoubleBuffered(false); if (borderColor != null) { if (borderWidth < 1) borderWidth = 1; setBorder(BorderFactory.createLineBorder(borderColor,borderWidth)); } else borderWidth = 0; if (preferredBlockWidth > 0 && preferredBlockHeight > 0) setPreferredSize(new Dimension(preferredBlockWidth*columns + 2*borderWidth, preferredBlockHeight*rows + 2*borderWidth)); } //--------- methods for getting and setting grid properties ---------- /** * Set the defaultColor. If c is null, the color will be set to black. * When a mosaic is first created, the defaultColor is black. * This is the color that is used for rectangles whose color * value is null. Such rectangles are always drawn as flat rather * than 3D rectangles. */ public void setDefaultColor(Color c) { if (c == null) c = Color.BLACK; if (! c.equals(defaultColor)) { defaultColor = c; setBackground(c); forceRedraw(); } } /** * Return the defaultColor, which cannot be null. */ public Color getDefaultColor() { return defaultColor; } /** * Set the color of the "grouting" that is drawn between rectangles. * If the value is null, no grouting is drawn and the rectangles * fill the entire grid. When a mosaic is first created, the * groutingColor is gray. */ public void setGroutingColor(Color c) { if (c == null || ! c.equals(groutingColor)) { groutingColor = c; forceRedraw(); } } /** * Get the current groutingColor, which can be null. */ public Color getGroutingColor(Color c) { return groutingColor; } /** * Set the value of alwaysDrawGrouting. If this is false, then * no grouting is drawn around rectangles whose color value is null. * When a mosaic is first created, the value is false. */ public void setAlwaysDrawGrouting(boolean always) { if (alwaysDrawGrouting != always) { alwaysDrawGrouting = always; forceRedraw(); } } /** * Get the value of the use3D property. */ public boolean getUse3D() { return use3D; } /** * Set the use3D property. When this property is true, the rectangles are * drawn as "3D" rects, which are supposed appear to be raised. When use3D * is false, they are drawn as regular "flat" rects. Note that flat rects * are always used for background squares that have not been assigned a color. * The default value of use3D is true; */ public void setUse3D(boolean use3D) { this.use3D = use3D; } /** * Get the value of the alwaysDrawGrouting property. */ public boolean getAlwaysDrawGrouting() { return alwaysDrawGrouting; } /** * Set the number of rows and columns in the grid. If the value of * the preserveData parameter is false, then the color values of all * the rectangles in the new grid are set to null. If it is true, * then as much color data as will fit is copied from the old grid. */ public void setGridSize(int rows, int columns, boolean preserveData) { if (rows > 0 && columns > 0) { Color[][] newGrid = new Color[rows][columns]; if (preserveData) { int rowMax = Math.min(rows,this.rows); int colMax = Math.min(columns,this.columns); for (int r = 0; r < rowMax; r++) for (int c = 0; c < colMax; c++) newGrid[r][c] = grid[r][c]; } grid = newGrid; this.rows = rows; this.columns = columns; forceRedraw(); } } /** * Return the number of rows of rectangles in the grid. */ public int getRowCount() { return rows; } /** * Return the number of columns of rectangles in the grid. */ public int getColumnCount() { return columns; } //------------------ other useful public methods --------------------- /** * Get the color which has been set for the rectangle in the specified * row and column of the grid. This value can be null if no * color has been set for that rectangle. (Such rectangles are * actually displayed using the defaultColor.) If the specified * rectangle is outside the grid, then null is returned. */ public Color getColor(int row, int col) { if (row >=0 && row < rows && col >= 0 && col < columns) return grid[row][col]; else return null; } /** * Return the red component of color of the rectangle in the * specified row and column. If that rectangle lies outside * the grid or if no color has been specified for the rectangle, * then the red component of the defaultColor is returned. */ public int getRed(int row, int col) { if (row >=0 && row < rows && col >= 0 && col < columns && grid[row][col] != null) return grid[row][col].getRed(); else return defaultColor.getRed(); } /** * Return the green component of color of the rectangle in the * specified row and column. If that rectangle lies outside * the grid or if no color has been specified for the rectangle, * then the green component of the defaultColor is returned. */ public int getGreen(int row, int col) { if (row >=0 && row < rows && col >= 0 && col < columns && grid[row][col] != null) return grid[row][col].getGreen(); else return defaultColor.getGreen(); } /** * Return the blue component of color of the rectangle in the * specified row and column. If that rectangle lies outside * the grid or if no color has been specified for the rectangle, * then the blue component of the defaultColor is returned. */ public int getBlue(int row, int col) { if (row >=0 && row < rows && col >= 0 && col < columns && grid[row][col] != null) return grid[row][col].getBlue(); else return defaultColor.getBlue(); } /** * Set the color of the rectangle in the specified row and column. * If the rectangle lies outside the grid, this is simply ignored. * The color can be null. Rectangles for which the color is null * will be displayed in the defaultColor, and they will always be shown * as flat rather than 3D rects. */ public void setColor(int row, int col, Color c) { if (row >=0 && row < rows && col >= 0 && col < columns) { grid[row][col] = c; if (OSI == null) repaint(); else { drawSquare(row,col,true); } } } /** * Set the color of the rectangle in the specified row and column. * The color is specified by giving red, green, and blue components * of the color. These values should be in the range from 0 to * 255, inclusive, and they are clamped to lie in that range. * If the rectangle lies outside the grid, this is simply ignored. */ public void setColor(int row, int col, int red, int green, int blue) { if (row >=0 && row < rows && col >= 0 && col < columns) { red = (red < 0)? 0 : ( (red > 255)? 255 : red); green = (green < 0)? 0 : ( (green > 255)? 255 : green); blue = (blue < 0)? 0 : ( (blue > 255)? 255 : blue); grid[row][col] = new Color(red,green,blue); if (OSI == null) repaint(); else { drawSquare(row,col,true); } } } /** * Set the color of the rectangle in the specified row and column. * The color is specified by giving hue, saturation, and brightness * components of the color. These values should be in the range from * 0.0 to 1.0, inclusive, and they are clamped to lie in that range. * If the rectangle lies outside the grid, this is simply ignored. */ public void setHSBColor(int row, int col, double hue, double saturation, double brightness) { if (row >=0 && row < rows && col >= 0 && col < columns) { grid[row][col] = makeHSBColor(hue,saturation,brightness); if (OSI == null) repaint(); else { drawSquare(row,col,true); } } } /** * A little utility routine that is provided for making a color * from hue, saturation, and brightness values. These values should * be in the range from 0.0 to 1.0, inclusive, and they are clamped * to lie in that range. (This method is more convenient than * Color.getHSBColor() since it use double values rather than float.) */ public static Color makeHSBColor( double hue, double saturation, double brightness) { float h = (float)hue; float s = (float)saturation; float b = (float)brightness; h = (h < 0)? 0 : ( (h > 1)? 1 : h ); s = (s < 0)? 0 : ( (s > 1)? 1 : s ); b = (b < 0)? 0 : ( (b > 1)? 1 : b ); return Color.getHSBColor(h,s,b); } /** * Set all rectangles of the grid to have the specified color. * The color can be null. In that case, the rectangles are * drawn as flat rather than 3D rects in the defaultColor. */ public void fill(Color c) { for (int i = 0; i < rows; i++) for (int j = 0; j < columns; j++) grid[i][j] = c; forceRedraw(); } /** * Set all rectangles of the grid to have the color specified by * the given red, green, and blue components. These components * should be integers in the range 0 to 255 and are clamped to lie * in that range. */ public void fill(int red, int green, int blue) { red = (red < 0)? 0 : ( (red > 255)? 255 : red); green = (green < 0)? 0 : ( (green > 255)? 255 : green); blue = (blue < 0)? 0 : ( (blue > 255)? 255 : blue); fill(new Color(red,green,blue)); } /** * Fill all the rectangles with randomly selected colors. */ public void fillRandomly() { for (int i = 0; i < rows; i++) for (int j = 0; j < columns; j++) { int r = (int)(256*Math.random()); int g = (int)(256*Math.random()); int b = (int)(256*Math.random()); grid[i][j] = new Color(r,g,b); } forceRedraw(); } /** * Clear the mosaic by setting all the colors to null. */ public void clear() { fill(null); } /** * Returns the current value of the autopaint property. * @see #setAutopaint(boolean) */ public boolean getAutopaint() { return autopaint; } /** * Sets the value of the autopaint property. When this property is true, * then every call to one of the setColor methods automatically results in * repainting that square on the screen. When it is desired to avoid this * immediate repaint -- for example, during a long sequence of setColors * that will all show up at once -- then the value of the autopaint property * can be set to false. When the value is false, color changes are recorded * in the data for the mosaic but are not made on the screen. When the * autopaint property is reset to true, the changes are applied and the * entire mosaic is repainted. The default value of this property is * true. *
Note that clearing or filling the mosaic will cause an immediate * screen update, even if autopaint is false. */ public void setAutopaint(boolean autopaint) { if (this.autopaint == autopaint) return; this.autopaint = autopaint; if (autopaint) forceRedraw(); } /** * This method can be called to force redrawing of the entire mosaic. The only * time it might be necessary for users of this class to recall this method is * while the autopaint property is set to false, and it is desired to show * all the changes that have been made to the mosaic, without resetting * the autopaint property to true. *@see #setAutopaint(boolean) */ final public void forceRedraw() { needsRedraw = true; repaint(); } /** * Return an object that contains the color data that * is needed to redraw the mosaic. This includes the * defaultColor, the groutingColor, the number of rows and * columns, the color of each rectangle, and the * value of alwaysDrawGrouting. */ public Object copyColorData() { // Note: This is a fudge. The data about defaultColor, // groutingColor, alwaysDrawGrouting, and use3D are added to the // last row of the grid. boolean values are represented by a // null for false and a non-null value for true. Color[][] copy = new Color[rows][columns]; // Replace the last row with a longer row. copy[rows-1] = new Color[columns+4]; for (int r = 0; r < rows; r++) for (int c = 0; c < columns; c++) copy[r][c] = grid[r][c]; copy[rows-1][columns] = defaultColor; copy[rows-1][columns+1] = groutingColor; copy[rows-1][columns+2] = (alwaysDrawGrouting? Color.BLACK : null); copy[rows-1][columns+3] = (use3D? Color.BLACK : null); return copy; } /** * The parameter to this method should be an Object that * was created by the copyColorData() method. This method * will restore the data in the object to the grid. This * can change the size of the grid, the colors in the grid, * the defaultColor, the groutingColor, and the value of * alwaysDrawGrouting. If the object is of the proper * form, then the return value is true. If not, the return * value is false and no changes are made to the current data. */ public boolean restoreColorData(Object data) { if (data == null || !(data instanceof Color[][])) return false; Color[][] newGrid = (Color[][])data; int newRows = newGrid.length; if (newRows == 0 || newGrid[0].length == 0) return false; int newColumns = newGrid[0].length; for (int r = 1; r < newRows-1; r++) if (newGrid[r].length != newColumns) return false; if (newGrid[newRows-1].length != newColumns+4) return false; if (newGrid[newRows-1][newColumns] == null) return false; rows = newRows; columns = newColumns; grid = new Color[rows][columns]; for (int i = 0; i < rows; i++) for (int j = 0; j < columns; j++) grid[i][j] = newGrid[i][j]; defaultColor = newGrid[newRows-1][newColumns]; setBackground(defaultColor); groutingColor = newGrid[newRows-1][newColumns+1]; alwaysDrawGrouting = newGrid[newRows-1][newColumns+2] != null; use3D = newGrid[newRows-1][newColumns+3] != null; forceRedraw(); return true; } /** * Given an x-coordinate of a pixel in the MosaicPanel, this method returns * the row number of the mosaic rectangle that contains that pixel. If * the x-coordinate does not lie within the bounds of the mosaic, the return * value is -1 or is equal to the number of columns, depending on whether * x is to the left or to the right of the mosaic. */ public int xCoordToColumnNumber(int x) { Insets insets = getInsets(); if (x < insets.left) return -1; double colWidth = (double)(getWidth()-insets.left-insets.right) / columns; int col = (int)( (x-insets.left) / colWidth); if (col >= columns) return columns; else return col; } /** * Given a y-coordinate of a pixel in the MosaicPanel, this method returns * the column number of the mosaic rectangle that contains that pixel. If * the y-coordinate does not lie within the bounds of the mosaic, the return * value is -1 or is equal to the number of rows, depending on whether * y is above or below the mosaic. */ public int yCoordToRowNumber(int y) { Insets insets = getInsets(); if (y < insets.top) return -1; double rowHeight = (double)(getHeight()-insets.top-insets.bottom) / rows; int row = (int)( (y-insets.top) / rowHeight); if (row >= rows) return rows; else return row; } /** * Returns the BufferedImage that contains the actual image of the mosaic. * If this is called before the mosaic has been drawn on screen, the return value will be null. */ public BufferedImage getImage() { return OSI; } //--------------- implementation details ------------------------ //---------- (routines called internally or by the system) ------ public void paintComponent(Graphics g) { super.paintComponent(g); synchronized(this) { if ( (OSI == null) || OSI.getWidth() != getWidth() || OSI.getHeight() != getHeight() ) { OSI = new BufferedImage(getWidth(), getHeight(), BufferedImage.TYPE_INT_RGB); OSG = OSI.getGraphics(); needsRedraw = true; } } if (needsRedraw) { for (int r = 0; r < rows; r++) for (int c = 0; c < columns; c++) drawSquare(r,c,false); needsRedraw = false; } g.drawImage(OSI,0,0,null); } synchronized private void drawSquare(int row, int col, boolean callRepaint) { // Draw one of the rectangles in a specified graphics // context. g must be non-null and (row,col) must be // in the grid. if (callRepaint && !autopaint) return; Insets insets = getInsets(); double rowHeight = (double)(getHeight()-insets.left-insets.right) / rows; double colWidth = (double)(getWidth()-insets.top-insets.bottom) / columns; int xOffset = insets.left; int yOffset = insets.top; int y = yOffset + (int)Math.round(rowHeight*row); int h = Math.max(1, (int)Math.round(rowHeight*(row+1))+yOffset - y); int x = xOffset + (int)Math.round(colWidth*col); int w = Math.max(1, (int)Math.round(colWidth*(col+1))+xOffset - x); Color c = grid[row][col]; OSG.setColor( (c == null)? defaultColor : c ); if (groutingColor == null || (c == null && !alwaysDrawGrouting)) { if (!use3D || c == null) OSG.fillRect(x,y,w,h); else OSG.fill3DRect(x,y,w,h,true); } else { if (!use3D || c == null) OSG.fillRect(x+1,y+1,w-2,h-2); else OSG.fill3DRect(x+1,y+1,w-2,h-2,true); OSG.setColor(groutingColor); OSG.drawRect(x,y,w-1,h-1); } if (callRepaint) repaint(x,y,w,h); } } // end class MosaicPanel