Home > Articles > Programming > Java

  • Print
  • + Share This
This chapter is from the book

A Tiled-Image Viewer

Using the RenderedImageCanvas class described in the previous section, let's create a viewer that is capable of displaying large images. Let's also add a panel called Tile Grid to show the viewport position and size with respect to the image (see Figure 11.6). We'll also add one more way to pan the image: dragging the rectangle that represents the image to move the image within the viewport. To control the tile size, let's add two text fields for entering tile width and height.

FIGURE 11.6 An image viewer for viewing large images

In addition to the classes mentioned in the JAI planar-image viewer example (see Chapter 10), here are the classes used in this application:

  • TiledImageViewer. This is the main class that launches the application. It is a subclass of JAIImageViewer.

  • RenderedImageCanvas. This class displays the image. It is a subclass of JAIImageCanvas (see the section titled Viewing and Manipulating Large Images earlier in this chapter).

  • RenderGrid. This class implements the Tile Grid panel. It displays the tile grid, as well as the viewport as a rectangle positioned over the tile grid. The position of this rectangle varies when you scroll the image. The position of the image changes when you drag this rectangle.

Now let's look at the code. We have already discussed the RenderedImageCanvas code, so let's look at the RenderGrid class. Even though this class uses no JAI API, we list it here because it draws heavily from the Java 2D graphical and rendering APIs. Listing 11.5 shows the code for RenderGrid.

LISTING 11.5 The RenderGrid class

package com.vistech.jai.render;
import java.awt.*;
import java.awt.event.*;
import java.awt.image.*;
import java.io.*;
import javax.swing.*;
import java.awt.geom.*;

public class RenderGrid extends JPanel {
  protected int maxTileIndexX, maxTileIndexY;
  protected int tileWidth, tileHeight;
  protected Point vpPos = new Point(0,0);
  protected Rectangle currentShape;
  protected int panX =0, panY =0;
  protected int width, height;
  protected boolean dragOn = false;
  private Point scrollAnchor = new Point(0,0);
  protected AffineTransform atx = new AffineTransform();
  protected Dimension vpSize;

  public RenderGrid() {
   enableEvents(AWTEvent.MOUSE_MOTION_EVENT_MASK |
             AWTEvent.MOUSE_EVENT_MASK);
  }

  public Dimension getViewportDimension(){
   return new Dimension(width, height);
  }

  public void setTileIndices(int maxXIndex, int maxYIndex){
   this.maxTileIndexX = maxXIndex;
   this.maxTileIndexY = maxYIndex;
   repaint();
  }

  public void setTileDimension(int width, int height){
   tileWidth = width; tileHeight = height;
   repaint();
  }

  public void setTileWidth(int tw){
   tileWidth = tw; repaint();
  }
  public int getTileWidth(){ return tileWidth;}

  public void setTileHeight(int th){
   tileHeight = th; repaint();
  }
  public int getTileHeight(){ return tileHeight;}
  public void setMaxTileIndexX(int tix){
   maxTileIndexX = tix; repaint();
  }

  public int getMaxTileIndexX(){return maxTileIndexX;}

  public void setMaxTileIndexY(int tiy){
   maxTileIndexY = tiy; repaint();
  }
  public int getMaxTileIndexY(){return maxTileIndexY;}
  public void setTransform(AffineTransform at){atx = at;}
  public void setViewportSize(Dimension size){vpSize = size;}

  public void processMouseEvent(MouseEvent e){
   switch(e.getID()){
     case MouseEvent.MOUSE_PRESSED:
       setCursor(new Cursor(Cursor.MOVE_CURSOR));
       int x = e.getX();
       int y = e.getY();
       if(currentShape.contains(x,y)) { dragOn = true;}
       break;
     case MouseEvent.MOUSE_CLICKED:
       break;
     case MouseEvent.MOUSE_RELEASED:
       setCursor(Cursor.getDefaultCursor());
       dragOn = false;
       break;
   }
  }

  public void processMouseMotionEvent(MouseEvent e){
    switch(e.getID()){
     case MouseEvent.MOUSE_DRAGGED:
       if(dragOn) scroll(e.getX(), e.getY());
       break;
   }
  }

  public void stopScroll(){ setCursor(Cursor.getDefaultCursor());}

  public void startScroll(int x, int y){
   if(dragOn) return;
   scrollAnchor.x = x- panX;
   scrollAnchor.y = y- panY;
   repaint();
  }

  public void scroll(int x, int y){
   panX = x-scrollAnchor.x;
   panY = y-scrollAnchor.y;
   setPan(new Point(panX, panY));
   repaint();
  }

  public void setPan(Point pos){
   firePropertyChange("viewportPosition",this.vpPos, pos);
   int x = (int)( pos.x * (maxTileIndexX*tileWidth)/(width-20));
   int y = (int)( pos.y * (maxTileIndexY*tileHeight)/(height-20));
   this.vpPos = new Point(x,y);
   repaint();
  }

  public void setViewportPosition(Point vpPos){
   this.vpPos = vpPos; repaint();
  }

  public void paintComponent(Graphics gc) {
   Graphics2D g = (Graphics2D)gc;
   Rectangle bounds = this.getBounds();
   g.setColor(Color.black);
   width = bounds.width-20;
   height = bounds.height-20;
   double gridWidth = (width)/(double)maxTileIndexX;
   double gridHeight = (height)/(double)maxTileIndexY;
   g.fillRect(0,0,bounds.width, bounds.height);
   if ((maxTileIndexX == 0)|| (maxTileIndexY == 0)) return;
   g.setColor(Color.blue);
   double vertStartX = 10.0; double vertStartY = 10.0;
   double vertEndX = 10.0;
   double vertEndY = (double)maxTileIndexY*gridHeight+vertStartY;
   // Horizontal lines
   for(int i= 0; i<maxTileIndexX; i++) {
     g.drawLine((int)vertStartX, (int)vertStartY, (int)vertEndX, (int)vertEndY);
     vertStartX += gridWidth;
     vertEndX  += gridWidth;
   }
   g.drawLine((int)vertStartX, (int)vertStartY,
         (int)vertEndX, (int)vertEndY);
   double horizStartX = 10.0; double horizStartY = 10.0;
   double horizEndX = maxTileIndexX*gridWidth+horizStartX;
   double horizEndY = 10.0;
   // Horizontal lines
   for(int i=0; i<maxTileIndexY; i++) {
     g.drawLine((int)horizStartX, (int)horizStartY,
           (int)horizEndX, (int)horizEndY);
     horizEndY += gridHeight;
     horizStartY  += gridHeight;
   }
   g.drawLine((int)horizStartX, (int)horizStartY,
         (int)horizEndX, (int)horizEndY);
   g.setColor(Color.red);
   vertStartX = 10.0; vertStartY = 10.0;
   int vpWid, vpHt;
   if(vpSize == null){ vpWid = (int)gridWidth; vpHt = (int)gridHeight;}
   else {
     vpWid = vpSize.width; vpHt = vpSize.height;
   }

   if(dragOn) {
     currentShape = new Rectangle(-(int)vertStartX+panX,
                   -(int)vertStartY+panY,vpWid, vpHt);
    }else {
     int x = (int)(vertStartX +
         vpPos.x * ((width)/(double)(maxTileIndexX*tileWidth)));
     int y = (int)(vertStartY +
         vpPos.y * ((height)/(double)(maxTileIndexY*tileHeight)));
     currentShape = new Rectangle(x,y,vpWid,vpHt);
   }

   g.draw(currentShape);
  }
}

The RenderGrid class handles mouse and mouseMotion events internally. When the mouse is pressed, the processMouseEvent() method checks whether the position of the cursor is within the rectangle. If so, it sets the dragOn variable to true. When the mouse is dragged, the processMouseMotion() method moves the rectangle. This method also calls the setPan() method, which fires the propertyChange event for the viewportPosition property.

The use of propertyChange events eliminates the need to call RenderedImage Canvas directly. An interposing object can receive this event and invoke appropriate methods in RenderedImageCanvas. In this case, the interposing object is the Tiled ImageViewer object, which upon receipt of this event, calls the setViewport Position() method of RenderedImageCanvas.

Likewise, when the mouse is dragged over the image, RenderedImageCanvas fires the propertyChange event for its viewportPosition property. Again this event is captured by the TiledImageViewer object, which in turn invokes RenderGrid's setViewportPosition() method. Thus, by using propertyChange events, you can avoid circular dependency between RenderGrid and RenderedImageCanvas. Moreover, both RenderedImageCanvas and RenderGrid can be used as beans and can be visually connected by binding properties (see Appendix B).

The paintComponent() method draws first the grid and then the rectangle representing the viewport. If the value of dragOn is true, which means the user is moving the rectangle, the panX and panY variables are used for moving the rectangle. If it is false, vpPos is used for moving the rectangle. The vpPos variable is set by the setViewportPosition() method, which is called by TiledImageViewer whenever it receives a propertyChange event for the viewportPosition property. The TiledImage Viewer object also sets the tile dimensions, on the basis of which RenderGrid draws the grid. Listing 11.6 shows the code for TiledImageViewer.

LISTING 11.6 The TiledImageViewer class: Part 1 (continued in Listing 11.7)

public class TiledImageViewer extends JAIImageViewer{
  protected RenderedImageCanvas viewer;
  protected RenderGrid renderGrid;
  protected JTextField twidth, theight;
  protected JTextArea memoryMessageBar;

  public void createUI() {
   Dimension dim = Toolkit.getDefaultToolkit().getScreenSize();
   int width = (int)(dim.width *3/4.0);
   int height = (int)(dim.height*3/4.0);

   setTitle("Rendering JAI Images");
   viewer = new RenderedImageCanvas();
   Dimension d = getViewerSize(width/(double)height);
   viewer.setPreferredSize(new Dimension(d.width, d.height));

   createImageLoaderAndSaver();
   loader.addPlanarImageLoadedListener(
     new PlanarImageLoadedListener() {
       public void imageLoaded(PlanarImageLoadedEvent e) {
        PlanarImage image = e.getImage();
        if(image == null) return;
        setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
        SwingUtilities.invokeLater(new ImagePaint(image));
       }
     }
   );

   twidth = new JTextField(5);
   theight = new JTextField(5);
   renderGrid = new RenderGrid();
   renderGrid.setBorder(BorderFactory.createTiledBorder("Tile Grid"));
   theight.setText(Integer.toString(viewer.getTileHeight()));
   twidth.setText(Integer.toString(viewer.getTileWidth()));
      //... Gridbag layout code not shown.

  }

  protected void addPropertyChangeEventAdapters(){
    viewer.addPropertyChangeListener(
     new PropertyChangeListener() {
       public void propertyChange(PropertyChangeEvent e) {
        if(e.getPropertyName().equals("viewportPosition")){
          twidth.setText(Integer.toString(viewer.getTileWidth()));
          theight.setText(Integer.toString(viewer.getTileHeight()));
          if(renderGrid != null) updateRenderGrid();
          Point p =(Point)e.getNewValue();
          renderGrid.setViewportPosition(p);
          }
          if(e.getPropertyName().equals("maxTileIndexX")){
           renderGrid.setMaxTileIndexX(((Integer)e.getNewValue()).            intValue());
          }
          if(e.getPropertyName().equals("maxTileIndexY")){
           renderGrid.setMaxTileIndexY(((Integer)e.getNewValue()).            intValue());
          }
          if(e.getPropertyName().equals("tileWidth")){
           renderGrid.setTileWidth(((Integer)e.getNewValue()).intValue());
          }
         if(e.getPropertyName().equals("tileHeight")){
           renderGrid.setTileHeight(((Integer)e.getNewValue()).intValue());
         }
         if(e.getPropertyName().equals("transform")){
           int wid = renderGrid.getViewportDimension().width;
           int ht = renderGrid.getViewportDimension().height;
           double scaleX = wid/(double)viewer.getSize().width;
           double scaleY = ht/(double)viewer.getSize().height;
           AffineTransform atx = (AffineTransform)e.getNewValue();
           renderGrid.setTransform(atx);
         }
      }
     }
   );

   renderGrid.addPropertyChangeListener(
     new PropertyChangeListener() {
       public void propertyChange(PropertyChangeEvent e) {
        if(e.getPropertyName().equals("viewportPosition")){
          double scaleX, scaleY;
          int wid = renderGrid.getViewportDimension().width;
          int ht = renderGrid.getViewportDimension().height;
          scaleX = viewer.getImageWidth()/wid;
          scaleY = viewer.getImageHeight()/ht;
          if(e.getNewValue() instanceof Point){
           Point p =(Point)e.getNewValue();
           viewer.pan(-p.x*scaleX, -p.y*scaleY);
         }
        }

      }
     }
   );
  }

  protected void updateRenderGrid() {
   if(viewer == null) return;
   int tw1 = viewer.getTileWidth();
   int th1 = viewer.getTileHeight();
   int xInd1 = viewer.getMaxTileIndexX();
   int yInd1 = viewer.getMaxTileIndexY();
   renderGrid.setTileDimension(tw1,th1);
   renderGrid.setTileIndices(xInd1, yInd1);
   int wid = renderGrid.getViewportDimension().width;
   int ht = renderGrid.getViewportDimension().height;
   double scaleX = viewer.getSize().width/(double)viewer.getImageWidth();
   double scaleY = viewer.getSize().height/(double)viewer.getImageHeight();
   double scaledVpX = wid*scaleX;
   double scaledVpY = ht*scaleY;
   renderGrid.setViewportSize(new Dimension((int)scaledVpX,(int)scaledVpY));
   renderGrid.repaint();
  }
}

TiledImageViewer is the main application class that launches the application. To run the application, type "java app.TiledImageViewer" or "java app.TiledImageViewer <image path>" on the command line.

The TiledImageViewer class extends JAIImageViewer and overrides the createUI() method. As Figure 11.6 shows, the left-hand panel of the window that represents this application has the Tile Grid pane and two text fields, for setting the tile width and height.

TiledImageViewer acts as an event adapter between RenderGrid and RenderedImageCanvas objects. When the GUI is created, TiledImageViewer registers to receive the propertyChange events from both RenderGrid and RenderedImage Canvas objects. The call to the addPropertyChangeEventAdapters() method in Listing 11.6 shows that this registration is done through anonymous inner classes.

When a user drags the mouse over the image, RenderImageCanvas fires a propertyChange event for the viewportPosition property. When TiledImageViewer receives this event, it calls the setViewportPosition() method in the RenderGrid object. Likewise, when a user moves the rectangle in the Tile Grid pane, the TiledImageViewer object receives the propertyChange event for the viewport Position property. In this case TiledImageViewer calls the pan() method in RenderedImageCanvas. Figure 11.7 shows how TiledImageViewer facilitates communication between RenderGrid and RenderedImageCanvas via propertyChange events.

FIGURE 11.7 The propertyChange event flow

TiledImageViewer acts as an event adapter for other properties as well, including tileWidth, tileHeight, maxTileIndexX, and maxTileIndexY.

Setting the Tile Parameters

You can change the tile layout by entering the tile width and height in the text fields located at the bottom of the left-hand pane. This pane also has a memory status bar that shows the total available and free memory. The memory status is updated whenever the mouse is clicked on the image. Note that the memory readings may not always be accurate. Listing 11.7 shows the code that handles events from the text fields and the code that updates the memory status bar.

LISTING 11.7 The TiledImageViewer class: Part 2 (continued from Listing 11.6)

  public JPanel createTileSetPanel(){
   memoryMessageBar = createMemoryMessageBar();
   memoryMessageBar.setBorder(BorderFactory.createTiledBorder("Memory"));
   //..Gridbag layout code

   return pan;
  }

  protected void addTileParamsEventAdapters(){
   twidth.addActionListener(
     new ActionListener(){
      public void actionPerformed(ActionEvent e) {
        try {
         String str = ((JTextField) e.getSource()).getText();
         int wid = Integer.parseInt(str);
         viewer.setTileWidth(wid);
         str = theight.getText();
         int ht = Integer.parseInt(str);
         viewer.setTileHeight(wid);
         if(renderGrid != null) updateRenderGrid();
        } catch (Exception e2){}
       }
     } );
   theight.addActionListener(
     new ActionListener(){
      public void actionPerformed(ActionEvent e) {
        try {
         String str = ((JTextField) e.getSource()).getText();
         int ht = Integer.parseInt(str);
         viewer.setTileHeight(ht);
         str = twidth.getText();
         int wid = Integer.parseInt(str);
         viewer.setTileWidth(wid);
         if(renderGrid != null) updateRenderGrid();
        } catch (Exception e1){}
       }
     });

   viewer.addMouseListener(
     new MouseAdapter() {
      public void mouseReleased(MouseEvent e){
       updateMemoryMessageBar(memoryMessageBar);
      }
     });
  }

  protected JTextArea createMemoryMessageBar(){
   JTextArea messageBar = new JTextArea();
   messageBar.setBackground(Color.black);
   messageBar.setForeground(Color.green);
   updateMemoryMessageBar(messageBar);
   return messageBar;
  }

  protected void updateMemoryMessageBar(JTextArea mbar) {
   Runtime rt = Runtime.getRuntime();
   long totalMemory = rt.totalMemory();
   rt.gc();
   long freemem = rt.freeMemory();
   mbar.setText("Total = "+ totalMemory+ "\n" +
          "Free = " + freemem);
  }

The updateMemoryMessageBar() method calls some utility methods in the java.lang.Runtime class to obtain the amount of total available memory and free memory. It also calls the System.gc() method to run the garbage collector. As we learned in Chapter 10, however, calling this method does not guarantee that the garbage collector will run. The updateMemoryMessageBar() method posts the amount of free memory to the memory message bar.

Painting the Image in a Thread

The TiledImageViewer class also overrides the ImagePaint class, which is shown in Listing 11.8. Running the image-painting routine on a thread is essential for painting large images.

LISTING 11.8 The ImagePaint class

class ImagePaint implements Runnable {
   PlanarImage image;
   boolean firstTime = true;
   public ImagePaint(PlanarImage image){this.image = image;}
   public void run() {
     if(firstTime) {
      try {
       firstTime = false;
       setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
       viewer.setImage(image);
       int wid = image.getWidth();
       int height = image.getHeight();
       saver.setDisplayImage(viewer.getDisplayImage());
       viewer.repaint();
       SwingUtilities.invokeLater(this);
      } catch(Exception e){SwingUtilities.invokeLater(this);}
     }
     else {
      if(!viewer.isImageDrawn()) SwingUtilities.invokeLater(this);
      else{
        setCursor(Cursor.getDefaultCursor());
        updateRenderGrid();
        updateMemoryMessageBar(memoryMessageBar);
      }
     }
   }
  }

The ImagePaint thread is created by an anonymous class that retrieves the image from the PlanarImageLoaded event object (see the createUI() method in Listing 11.6). In the first pass, the run() method calls RenderedImageCanvas's setImage() method, which calls the repaint() method to start the image painting.

The SwingUtilities.invokeLater() method runs the ImagePaint thread repeatedly until isImageDrawn() returns true. When that happens, the run() method calls updateRenderGrid(), which gets the latest tile settings from the viewer and assigns them to renderGrid. It also updates the memory message bar and changes the hourglass cursor to the default cursor.

  • + Share This
  • 🔖 Save To Your Account