AsynchronousDisplayOfTiledImages < Javapedia < TWiki
|
TWiki . Javapedia . 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 "value" 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 "cache" 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();
}
}
}
----- Revision r1 - 30 Jan 2007 - 21:09:52 - Main.rummyr
|