AsynchronousDisplayOfTiledImages < Javapedia < TWiki

TWiki . Javapedia . AsynchronousDisplayOfTiledImages

Home | Help | Changes | Index | Search | Go

AsynchronousDisplayOfTiledImages

package googleMapTool.utils;

import java.awt.Container;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.event.MouseEvent;
import java.awt.event.MouseMotionAdapter;
import java.awt.geom.AffineTransform;
import java.awt.image.ColorModel;
import java.awt.image.Raster;
import java.awt.image.RenderedImage;
import java.awt.image.SampleModel;
import java.awt.image.WritableRaster;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Vector;
import java.util.logging.ConsoleHandler;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.media.jai.JAI;
import javax.media.jai.PlanarImage;
import javax.media.jai.TileCache;
import javax.media.jai.TileComputationListener;
import javax.media.jai.TileRequest;
import javax.media.jai.TileScheduler;
import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.JScrollPane;
import javax.swing.SwingUtilities;

/**
/** Some IP in this class may apply to SUN since I have previously read the ImageCanvas code.
 * The licence on ImageCanvas appears to suggest that this is generally OK.
 * Ther than that this code is released as Public Domain.
 * This component is always Opaque.
 * This component <em>always</em> draws the image 0,0 at the top left. If you want to draw offsetted images
 * you will have to use code like:
 * <code>JAI.create("translate",im,new Float(30.0f), new Float(-40.0f))</code>
 * or more properly
 * <code>
 *       ParameterBlock pb = new ParameterBlock();
 *      pb.addSource(img);
 *       pb.add(30.0f);
 *      pb.add(-40.0f);
 *      RenderedImage im = JAI.create("translate", pb, null);
 * </code>
 * 
 * 
 */
public class NewJAIDisplay
   extends JComponent
   implements TileComputationListener
{
   private Logger logger = Logger.getLogger(NewJAIDisplay.class.getName());

   /** A list of Points that represent tiles that are scheduled to be computed.
    * The &quot;value&quot; is the TileRquest generated by the schedule and is needed to cancel tiles.
   */
   private final Map scheduledTiles = Collections.synchronizedMap(new HashMap());
   
   /** A bucket to store rasters, tiles that are not visible at the end of a repaint are cleared, 
    *  This is implemented to help with entertaining redraw issues when the tile cache size is set to zero.
    *  I don't like keeping a seperate &quot;cache&quot; but it seems to be neccessary when the tile cache 
    *  is too small to preseve the raster between computation and repaint. */
   private final Map rasterBucket = Collections.synchronizedMap(new HashMap());

   /** The source RenderedImage. */
   private PlanarImage im;

   {
      ConsoleHandler ch = new ConsoleHandler();
      //ch.setLevel(Level.FINEST);  
      logger.addHandler(ch);
     //logger.setLevel(Level.FINEST);
   }

   /**
    * @param im the image to display with 0,0 at the top left.
    */
   public NewJAIDisplay(RenderedImage im) {
      // wrap as a planar image so we can leverage some of the more fun methods like getTileIndex() 
      this.im = PlanarImage.wrapRenderedImage(im); // indempotent if input image is a PlanarImage
      logger.finest("Image is from " + im.getMinX() + "," + im.getMinY() + " " + im.getWidth() + "x" + im.getHeight());
   }


   public Dimension getMaximumSize() {
      return getMinimumSize();
   }

   public Dimension getMinimumSize() {
      return new Dimension(im.getMinX() + im.getWidth(), im.getMinY() + im.getHeight());
   }


   public Dimension getPreferredSize() {
      return getMinimumSize();
   }


   public static void main(String[] args) {
      String imgSrc = "4thOS71 - Kidderminster - 1921.tiff";

      if (args.length != 0) {
         imgSrc = args[0];
      }

      JFrame f    = new JFrame("NewJAIDisplay Test");
      Container c = f.getContentPane();
     RenderedImage img = JAI.create("fileload", imgSrc);

      final JComponent display = new NewJAIDisplay(JAI.create("translate",img,new Float(30.0f), new Float(-40.0f)));
      display.addMouseMotionListener(new MouseMotionAdapter() {
         public void mouseMoved(MouseEvent e) {
            display.setToolTipText(e.getX() + "," + e.getY());
         }
      });
      JScrollPane sp     = new JScrollPane(display);
      System.out.println("Scroll Mode is " + sp.getViewport().getScrollMode());

      c.add(sp);
      f.setSize(200, 200);
      f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
      f.setVisible(true);
   }

   /* (non-Javadoc)
    * @see java.awt.Component#isOpaque()
    */
   public boolean isOpaque() {
      return true;
   }

   public synchronized void paintComponent(Graphics g) {
          boolean FINEST = logger.isLoggable(Level.FINEST);
          boolean FINE = logger.isLoggable(Level.FINE);
      if (FINE) {
         logger.fine("paintComponent called for" + g.getClipBounds());

         StringBuffer msg = new StringBuffer("Scheduled:");

         for (Iterator itr = scheduledTiles.keySet().iterator(); itr.hasNext();) {
            Point p = (Point) itr.next();
            msg.append('[').append(p.x).append(',').append(p.y).append(']').append(' ');
         }

         logger.fine(msg.toString());
      }

      if (im == null) {
         return;
      }

      Graphics2D g2D = null;

      if (g instanceof Graphics2D) {
         g2D = (Graphics2D) g.create();
      }
      else {
         System.err.println("Requires a Graphics2D!");

         return;
      }


      // Get the clipping rectangle and translate it into image coordinates. 
      Rectangle clipBounds = g2D.getClipBounds();

      if (clipBounds == null) {
         // what!
         //TODO is this needed?
         clipBounds = new Rectangle(0, 0, im.getWidth(), im.getHeight());
      }

      if (isOpaque()) {
         g2D.setColor(getBackground());
         g2D.fillRect(clipBounds.x, clipBounds.y, clipBounds.width, clipBounds.height);
      }


      // Get all tiles which overlap the clipping region.
      Point[] tileIndices;

      if (true) {
         tileIndices = im.getTileIndices(clipBounds);
      }
      
      if (tileIndices == null || tileIndices.length == 0) {
         // nothing to do
         if (FINEST) {
            logger.finest("quick return because there are no tiles");
         }
         return;
      }

      // only perform scheduling if there are tiles to draw
      if (tileIndices.length > 0) {
         ArrayList inCache            = new ArrayList(tileIndices.length);
         ArrayList toSchedule         = new ArrayList(tileIndices.length);
         ArrayList currentlyScheduled = new ArrayList(tileIndices.length);

         TileCache cache = JAI.getDefaultInstance().getTileCache();

         // build 3 lists
         // a in cache or in the "tilesInView" list
         // b not in cache
         // c currently scheduled
         for (int tile = 0; tile < tileIndices.length; tile++) {
            Point p = tileIndices[tile];
         Raster r;
         if (rasterBucket.containsKey(p)) {
            inCache.add(p); // actually in the rasterBucket, but close enough.
         }
            else if ((r = cache.getTile(im, p.x, p.y)) != null) {
               inCache.add(p);
               rasterBucket.put(p, r);
            }
            else if (scheduledTiles.containsKey(p)) {
               currentlyScheduled.add(p);
            }
            else { // not in cache, not already scheduled
               toSchedule.add(p);
            }
         }

         // replace tileIndices with the items in the cache/rasterBucket
         tileIndices = (Point[]) inCache.toArray(new Point[inCache.size()]);

         if (!toSchedule.isEmpty()) { // schedule some new tiles

            TileScheduler scheduler   = JAI.getDefaultInstance().getTileScheduler();
            Point[] toScheduleIndices = (Point[]) toSchedule.toArray(new Point[toSchedule.size()]);

            if (FINE) {
               StringBuffer msg = new StringBuffer("Scheduling:");

               for (Iterator itr = toSchedule.iterator(); itr.hasNext();) {
                  Point p = (Point) itr.next();
                  msg.append('[').append(p.x).append(',').append(p.y).append(']').append('\n');
               }

               logger.fine(msg.toString());
            }

            TileRequest request = scheduler.scheduleTiles(im, toScheduleIndices, new TileComputationListener[] {this});

            for (int i = 0; i < toScheduleIndices.length; i++) {
               scheduledTiles.put(toScheduleIndices[i], request);
            }
         }

         if (FINE) {
            logger.fine("in cache:" + inCache);
            logger.fine("currently Scheduled:" + currentlyScheduled);
            logger.fine("toSchedule:" + toSchedule);
         }
      }



      // Loop over tiles (all these tiles are within the clipping region)
      int numTiles = tileIndices.length;
      AffineTransform identityAT = new AffineTransform();
      // for performance reasons? we create a re-usable object that wraps a raster and makes it look like a
      // rendered image. It may be premature optimization, but I know creating a buffered image
      // can create a WritableRaster from the Raster. 

      OneRasterRenderedImage ri = new OneRasterRenderedImage(im.getColorModel());
      

      for (int tileNum = 0; tileNum < numTiles; tileNum++) {
         Raster raster = (Raster)rasterBucket.get(tileIndices[tileNum]);
         ri.setRaster(raster);
         
         if (FINEST) {
            logger.finest("drawing tile:" + tileIndices[tileNum].x + "," + tileIndices[tileNum].y
               + "at " + raster.getMinX() + "," + raster.getMinX() + " + offset" );
         }
       //g2D.drawRenderedImage(ri, AffineTransform.getTranslateInstance(-im.getMinX(), -im.getMinY()));
      g2D.drawRenderedImage(ri, identityAT);
      }


      // cancel tiles that are not in the view
      Rectangle visible = getVisibleRect();

      if (FINE) {
         logger.fine("Visible Rect:" + visible);
      }

      Point[] tilesInViewArray = im.getTileIndices(visible);
      Collection tilesInView         = new HashSet(Arrays.asList(tilesInViewArray));
      
      cancelScheduledTilesOutOfView(tilesInView);

      
     pruneRasterBucket(tilesInView);

   }

   /**
    *  Cancels any tiles that are in the scheduled list and not in the tilesToRetain list
    */
private void cancelScheduledTilesOutOfView(Collection tilesInView) {
   final boolean FINE = logger.isLoggable(Level.FINE);
   TileScheduler scheduler  = JAI.getDefaultInstance().getTileScheduler();

      // take a copy of the currently scheduled tiles 
      // to avoid concurrent modifications
      // we're not removing anything from this list here so a copy should be fine.
      final Map scheduledTilesCopy;

      synchronized (scheduledTiles) {
         scheduledTilesCopy = new HashMap(scheduledTiles);
      }

      // scan through the scheduled tiles list cancelling any that are no longer in view
      final StringBuffer waitingMsg;
      
      if (FINE) {
         waitingMsg = new StringBuffer("Waiting for:");
      }
      else {
         waitingMsg = null;
      }

      for (Iterator itr = scheduledTilesCopy.keySet().iterator(); itr.hasNext();) {
         Point p = (Point) itr.next();

         if (!tilesInView.contains(p)) {
            if (FINE) {
               logger.fine("Cancel " + p);
            }

            TileRequest request = (TileRequest) scheduledTiles.get(p);

            if (request == null) {
               // been removed somehow! Doesn't normally happen, but we are working on a copy in a multi thread world - don't worry about it.
               continue;
            }

            scheduler.cancelTiles(request, new Point[] {p});
            // and remove from the scheduled list in case a race condition meant the tile got computed anyway
            scheduledTiles.remove(p);
         }
         else {
            if (FINE) {
               waitingMsg.append('[').append(p.x).append(',').append(p.y).append(']').append(' ');
            }
         }
      }
      if (FINE) {
          logger.fine(waitingMsg.toString());
       }

}

/**
 *  cleans up the rasterBucket, removing any tiles that aren't in the
 * tilesToRetain list
 */

private void pruneRasterBucket(Collection tilesToRetain) {
   final boolean FINE = logger.isLoggable(Level.FINE); 
     final StringBuffer inBucketMsg;
     final Map rasterBucketCopy;
     synchronized (rasterBucket) {
       rasterBucketCopy = new HashMap(rasterBucket);
     }
     
      if (FINE) {
         inBucketMsg = new StringBuffer("inBucket:");
      }
      else {
         inBucketMsg = null;
      }

     for (Iterator itr = rasterBucketCopy.keySet().iterator(); itr.hasNext();) {
       Point p = (Point) itr.next();

       if (!tilesToRetain.contains(p)) {
         rasterBucket.remove(p);
         if (FINE) {
            logger.fine("remove from rasterBucket " + p);
         }
       }
       else {
         if (FINE) {
            inBucketMsg.append('[').append(p.x).append(',').append(p.y).append(']').append(' ');
         }
       }
     }
      if (FINE) {
         logger.fine(inBucketMsg.toString());
      }
}

   public void tileCancelled(Object arg0, TileRequest[] arg1, PlanarImage arg2, int x, int y) {
      if (logger.isLoggable(Level.FINE)) {
         logger.fine("TileCancelled:" + "scheduler:" + "," + arg1 + ", image" + "," + x + "," + y);
      }

     Point p = new Point(x, y);
      scheduledTiles.remove(p);
       rasterBucket.remove(p);

   }

   public void tileComputationFailure(Object arg0, TileRequest[] arg1, PlanarImage arg2, int x, int y,
                                      Throwable arg5) {
      if (logger.isLoggable(Level.FINE)) {
         logger.fine("TileComputationFailure:" + "scheduler:" + "," + arg1 + ", image" + "," + x + "," + y + ","
                     + arg5);
      }
      Point p = new Point(x,y);
      scheduledTiles.remove(p);
      rasterBucket.remove(p);
   }

   public void tileComputed(Object arg0, TileRequest[] arg1, PlanarImage arg2, int x, int y, final Raster raster) {
      //synchronized (paintLock)  // used to try a lock while painting, but this locks up the whole system.
      {
         if (logger.isLoggable(Level.FINE)) {
            logger.fine("Tile computed:" + "scheduler:" + "," + arg1 + ", image" + "," + x + "," + y + "," + "raster");
         }
   
         // add to the cache, just in case the original object didn't!
         JAI.getDefaultInstance().getTileCache().add(im, x, y, raster);
   
         // now issue a repaint request for the tile
        Point p = new Point(x,y);
         scheduledTiles.remove(p);
         rasterBucket.put(p, raster);
              if (logger.isLoggable(Level.FINE)) {
                 logger.fine("Raster location:" + raster.getBounds());
              }
              final Rectangle rasterBounds = raster.getBounds();
              
              // although the repaint is supposed to be thread safe
              // grey rectangles are left if it is called directly
              // if invoked later these problems disappear.
              SwingUtilities.invokeLater( new Runnable() {
                 public void run() {
               repaint(rasterBounds.x, rasterBounds.y, rasterBounds.width, rasterBounds.height); // just repaint the individual tile.
                 //repaint();
                 }
              });
      }
   }



   /** 
    * A OneRasterRenderedImage is a trivial class used for (premature) optimization.
    * Most code seems to create a buffered image from the raster for display, this can cause 
    * a spare memory allocation as the Raster may need copying to a Writable Raster. By using
    * this class and graphics2d.drawRenderedImage we can use the tile raster directly
    * without needing to create a buffered image. 
    *
    */
   private static class OneRasterRenderedImage implements RenderedImage {
      private Raster r;
      private final ColorModel cm;
      
      public OneRasterRenderedImage(ColorModel cm) {
         this.cm = cm;
      }
      
      public void setRaster(Raster newRaster) {
         r = newRaster;
      }
      public int getHeight() {
         return r.getHeight();
      }

      public int getMinTileX() {
         return 0;
      }

      public int getMinTileY() {
         return 0;
      }

      public int getMinX() {
         return r.getMinX();
      }

      public int getMinY() {
         return r.getMinY();
      }

      public int getNumXTiles() {
         return 1;
      }

      public int getNumYTiles() {
         return 1;
      }

      public int getTileGridXOffset() {
         return 0;
      }

      public int getTileGridYOffset() {
         return 0;
      }


      public int getWidth() {
         return r.getWidth();
      }

      public ColorModel getColorModel() {
         return cm;
      }

      public Raster getData() {
         return r;
      }

      public Raster getTile(int tileX, int tileY) {
         return r; // only ever 1 tile
      }

      public SampleModel getSampleModel() {
         return r.getSampleModel();
      }

      public String[] getPropertyNames() {
         return null;
      }

      public Vector getSources() {
         return null;
      }

      public Raster getData(Rectangle rect) {
         return r;
      }

      public WritableRaster copyData(WritableRaster raster) {
         return null;
      }

      public Object getProperty(String name) {
         return null;
      }

      public int getTileHeight() {
         return r.getHeight();
      }


      public int getTileWidth() {
         return r.getWidth();
      }
   }
}



Discussion about AsynchronousDisplayOfTiledImages

----- Revision r1 - 30 Jan 2007 - 21:09:52 - Main.rummyr