Understanding the Display Component Classes

There are two types of classes in the edu.kzoo.grid.display package: classes used to display a grid as a whole, and classes used to display the various objects in the grid. This document describes the classes that display individual grid objects first and then the classes used to display the overall grid.

Sections:

  1. GridObject Display Classes
  2. Grid Display Classes

GridObject Display Classes

The following is the layout of the general GridObject display classes:

Figure 1

The structure of the display hierarchy is much less complicated than it first appears.  GridObjectDisplay is a general interface that defines how all GridObjects will be displayed.  ScaledDisplay implements the interface and provides the structure for how to draw an object.  ScaledDisplay implements the draw() method that is defined by GridObjectDisplay.  The implementation is as follows:

 public void draw(GridObject obj, Component comp, Graphics2D g2, Rectangle rect) 
{       
    // Translate to center of object
g2.translate(rect.x + rect.width/2, rect.y + rect.height/2);
    // Scale to size of rectangle, adjust stroke back to 1-pixel wide
    float scaleFactor = Math.min(rect.width, rect.height);
    g2.scale(scaleFactor, scaleFactor);
    g2.setStroke(new BasicStroke(1.0f/scaleFactor));
    // Apply the decorators
    if (!decorations.isEmpty()) {
        for (int i = 0; i < decorations.size(); i++)
{
            ((DisplayDecorator)decorations.get(i)).decorate(this, obj, comp, g2);
        }         
    }
    // Adjust (e.g., rotate) as necessary.
    adjust(obj, comp, g2);

    // Draw the image
    draw(obj, comp, g2);
}
Code Sample 1

The method does four distinct things:

  1. It scales the image to fit into a a single grid location.
  2. It applies any decorators for the display object.
  3. It calls adjust().
  4. It calls the abstract draw() method. 
The separation into four steps allows for a great deal of customization. 

The scaling is done using the following algorithm:

  // Translate to center of object
  g2.translate(rect.x + rect.width/2, rect.y + rect.height/2);
// Scale to size of rectangle, adjust stroke back to 1-pixel wide
float scaleFactor = Math.min(rect.width, rect.height);
g2.scale(scaleFactor, scaleFactor);
g2.setStroke(new BasicStroke(1.0f/scaleFactor));
Code Sample 2

The DisplayDecorators are stored in an ArrayList defined in ScaledDisplay.  After the image has been scaled any decorators found in the ArrayList will be applied. (An explanation of how decorators work and how to design one will follow later.)

The adjust() method allows for another step of customization to the drawing algorithm.  This method pre-dates the use of display decorators, and is provided primarily to avoid breaking existing code.

The call to the second draw() method is used so that each subclass can define its own algorithm for drawing.  If there were only one draw() method all of the associated GridObjects would have to conform to the exact same specification.  For example: the ColorBlockDisplay calls the color() method on the associated GridObjectTextCellDisplay calls both color() and text() on the associated GridObject.

Example:
The ScaledImageDisplay class defines aditional instance variables:

  private ImageIcon icon;
  private Image originalImage;
  private DefaultDisplay defaultDisp;
  private HashMap tintedVersions = new HashMap();
Code Sample 3

It then references these properties in the defined draw method:

  public void draw(GridObject obj, Component comp, Graphics2D g2) 
{
   if (icon.getImageLoadStatus() != MediaTracker.COMPLETE) {
     // Image failed to load, so fall back to default display.
     defaultDisp.draw(obj, comp, g2);
        return;
    }
   
   // Scale to shrink or enlarge the image to fit the size 1x1 cell.
    g2.scale(1.0/icon.getIconWidth(), 1.0/icon.getIconHeight());
   icon.paintIcon(comp, g2, -icon.getIconWidth()/2, -icon.getIconHeight()/2);   
}
Code Sample 4

Decorators are another important aspect of the display package.  They allow further customization of the GridObject displays.  The use of decorators prevents an exponential explosion of new GridObjectDisplay subclasses whenever additional functionality is desired; additional decorators can simply be applied to existing ScaledDisplay objects. Note that the decorators were added at the ScaledDisplay level due to the preexisting structure of the draw() method.  There are also no display objects that are not scaled to fit into a single grid location.  The following is the structure of the DisplayDecorator hierarchy:
Figure 2

DisplayDecorator provides an interface that all decorators must conform to so that decorate() can be called from the draw() method in ScaledDisplay

RotatedDecorator, which allows the display for a GridObject with a direction() method to change direction along with the object, defines a number of new instance variables and methods.  The originalDirection instance variable keeps track of the direction the image originaly was facing.  This is used as a reference point in rotating the image.  The decorate() method is as follows:

    public void decorate(ScaledDisplay sd, GridObject obj, Component comp, Graphics2D g2) {
        // Rotate drawing surface to compensate for the direction of the object
        // in the image (in case it is not facing North, as assumed).
        if ( ! originalDirection.equals(Direction.NORTH) ) {
            int rotationInDegrees = originalDirection.inDegrees();
            g2.rotate(- Math.toRadians(rotationInDegrees));
            if (rotationInDegrees >= 180)
                g2.scale(1, -1); // flip image upside-down
        }
        // Now rotate again to represent the direction the object is facing.
        adjustForDirection(obj, g2);
    }

Code Sample 5


After adjustments are made depending on whether or not the object is facing north the image is adjusted for the actual direction with a call to adjustForDirection().

    public static void adjustForDirection(GridObject obj, Graphics2D g2)
    {       
        // Rotate drawing surface to capture object's orientation
        // (direction).  (Assumption is that without rotating the
        // drawing surface the object will be drawn facing North.)
        // Object must have a direction method.
        Class objClass = obj.getClass();
        try
        {
            Method dirMethod = objClass.getMethod("direction", new Class[0]);
            Direction dir = (Direction)dirMethod.invoke(obj, new Object[0]);
            int rotationInDegrees = dir.inDegrees();
            g2.rotate(Math.toRadians(rotationInDegrees));
            //return rotationInDegrees;
        }
        catch (NoSuchMethodException e)
        { throw new IllegalArgumentException("Cannot rotate object of " + objClass +
            " class; cannot invoke direction method."); }
        catch (InvocationTargetException e)
        { throw new IllegalArgumentException("Cannot rotate object of " + objClass +
            " class; exception in direction method."); }
        catch (IllegalAccessException e)
        { throw new IllegalArgumentException("Cannot rotate object of " + objClass +
            " class; cannot access direction method."); }
    }
Code Sample 6

At this point all of the necessary changes have been made to the display.  Program control will then flow back to the draw() method of the ScaledDisplay class.

Grid Display Classes

The display package also contains classes used to display an entire Grid.  There is one primary grid display class provided, the ScrollableGridDisplay class.  It implements two interfaces: GridDisplay and GridBackgroundDisplayScrollableGridDisplay extends javax.swing.JPanel.  The entire grid display structure is as follows:
Figure 3



GridDisplay requires a setGrid() method that sets the Grid that will be displayed as well as a showGrid() method that will display the GridGridBackgroundDisplay provides a single method for drawing the background for the grid.  ScrollableGridDisplay implements both of these interfaces becauses it defines its own drawBackground() method. 

The showGrid() method is the method called to display the grid, although almost none of the actual display code resides in that method.  It simply tells the display (which is a sbuclass of JPanel) to repaint itself.

    public void showGrid()
    {
        repaint();
    }
Code Sample 7

The method paintComponent() controls the actually drawing of the Grid.

    public void paintComponent(Graphics g)
    {
        Graphics2D g2 = (Graphics2D)g;      
        super.paintComponent(g2);
        if (grid() == null)
            return;      
        backgroundDisplay.drawBackground(g2);
        GridObject[] allGridObjects = grid().allObjects();
        for (int k = 0; k < allGridObjects.length; k++)
            drawGridObject(g2, allGridObjects[k]);

        if ( gridLinesAreVisible() )
            drawGridlines(g2);
    }
Code Sample 8

There are five steps involved in the method:
  1. Call the paintComponent() method of the super class (javax.swing.JPanel).
  2. Check to see if there is in fact a Grid defined for the display.
  3. Draw the background.
  4. Draw the GridObjects.
  5. Draw the grid lines if they are visible.
Drawing the background relies on the backgroundDisplay instance variable which is of type GridBackgroundDisplay.  The backgroundDisplay variable for ScrollableGridDisplay is set as a reference to itself by default.  That is why it was necessary for ScrollableGridDisplay to implement GridBackgroundDisplay.  If the backgroundDisplay object were set as a reference to a CheckeredBackgroundDisplay object then it would use the drawBackground() method defined in CheckeredBackgroundDisplay.

The drawBackground() method defined in ScrollableGridDisplay is as follows:

    public void drawBackground(Graphics2D g2)
    {
        fillBackground(g2, bgColor);
    }
Code Sample 9

It simply calls a method that will fill the entire background in with the same color:

    public void fillBackground(Graphics2D g2, Color fillColor)
    {
        Color oldColor = g2.getColor();
        Insets insets = getInsets();
        g2.setColor(fillColor);
        g2.fillRect(insets.left, insets.top,
                    numCols*outerCellSize + gridLineWidth,
                    numRows*outerCellSize + gridLineWidth);
        g2.setColor(oldColor);
    }
Code Sample 10

The drawBackground() method in the checkeredBackgroundDisplay class is more complicated, but does largely the same thing.  Rather than fill the entire background with a solid color it draws a checkerboard pattern using the colors defined in its instance variables (upperLeftColor and otherColor).

    public void drawBackground(Graphics2D g2){

        // Fill the background with one of the two colors.
overallDisplay.fillBackground(g2, otherColor);

        // Fill in the checkerboard pattern with the other color.
Insets insets = overallDisplay.getInsets();

        int leftSide;
int topSide;

        Grid grid = overallDisplay.grid();
        for (int row = 0; row < grid.numRows(); row++) {
            for (int col = 0; col < grid.numCols(); col++) {

                // Calculate upper left corner of the cell to draw.
                leftSide = overallDisplay.colToXCoord(col);
                topSide = overallDisplay.rowToYCoord(row);

                // Put the other checkered color in the top-left cell and
                // every other cell whose row and column are both even or
                // both odd.
                if ( (col % 2) == (row % 2 ) ) {
                    g2.setColor(upperLeftColor);
                    g2.fillRect(leftSide,  topSide,
                                overallDisplay.innerCellSize(),
                                overallDisplay.innerCellSize());
                }
            }
        }
    }
Code Sample 11

After the background has been drawn the display draws the GridObjects that are in theGrid.  All of the GridObjects are put in a temporary array and then looped through calling drawGridObject() for each one.

   protected void drawGridObject(Graphics2D g2, GridObject obj) 
{
        // Make sure that obj hasn't been removed from the grid
        // since it was placed in the list of objects to draw
        if ( obj.grid() != grid() )
            return;
        Location objLoc = obj.location();

        Rectangle cellToDraw =
            new Rectangle(colToXCoord(objLoc.col()),
                          rowToYCoord(objLoc.row()),
                          innerCellSize(), innerCellSize());
        // Only draw if the object is visible within the current clipping region.
        if (cellToDraw.intersects(g2.getClip().getBounds())) {   
            AffineTransform savedTransform = g2.getTransform(); // save

            // Get the drawing object to display this grid object.
            GridObjectDisplay displayObj = DisplayMap.findDisplayFor(obj);
            displayObj.draw(obj, this, g2, cellToDraw);
            g2.setTransform(savedTransform); // restore coordinate system
   } 
Code Sample 12

It first checks that the object is in the proper grid.  It then determines the cell in which it should be drawn in based on its Location in theGrid.  It then makes sure that the object will be visible within the cell.  If it is visible it performs the affine transformation on the graphics object (g2).  It then locates the correct GridObjectDisplay from DisplayMap and uses that GridObjectDisplay to draw the object in the correct cell.

The final step for drawing the background is the drawing of the grid lines if they are visible:

    protected void drawGridlines(Graphics2D g2){
        Rectangle curClip = g2.getClip().getBounds();
        int top = getInsets().top, left = getInsets().left;
        int cellSize = outerCellSize;
        int miny = Math.max(0, (curClip.y - top)/cellSize)*cellSize + top;
        int minx = Math.max(0, (curClip.x - left)/cellSize)*cellSize + left;
        int maxy = Math.min(numRows,(curClip.y + curClip.height - top + cellSize - 1)/cellSize)*cellSize + top;
        int maxx = Math.min(numCols, (curClip.x + curClip.width - left + cellSize - 1)/cellSize)*cellSize + left;

        g2.setColor(Color.black);
        g2.setStroke(new BasicStroke());

        for (int y = miny; y <= maxy; y += cellSize) // draw horizontal lines
            g2.drawLine(minx, y, maxx, y);

        for (int x = minx; x <= maxx; x += cellSize) // draw vertical lines
            g2.drawLine(x, miny, x, maxy);
    }
Code Sample 13

The locations of the lines are calculated using the size of the grid display and the number of rows and columns.