001: /*
002: * GeoTools - OpenSource mapping toolkit
003: * http://geotools.org
004: * (C) 2003-2006, Geotools Project Managment Committee (PMC)
005: * (C) 2003, Institut de Recherche pour le Développement
006: *
007: * This library is free software; you can redistribute it and/or
008: * modify it under the terms of the GNU Lesser General Public
009: * License as published by the Free Software Foundation; either
010: * version 2.1 of the License, or (at your option) any later version.
011: *
012: * This library is distributed in the hope that it will be useful,
013: * but WITHOUT ANY WARRANTY; without even the implied warranty of
014: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
015: * Lesser General Public License for more details.
016: */
017: package org.geotools.image;
018:
019: // J2SE dependencies
020: import java.awt.Color;
021: import java.awt.Point;
022: import java.awt.image.ColorModel;
023: import java.awt.image.DataBuffer;
024: import java.awt.image.DataBufferByte;
025: import java.awt.image.DataBufferDouble;
026: import java.awt.image.DataBufferFloat;
027: import java.awt.image.DataBufferInt;
028: import java.awt.image.DataBufferShort;
029: import java.awt.image.DataBufferUShort;
030: import java.awt.image.IndexColorModel;
031: import java.awt.image.Raster;
032: import java.awt.image.RasterFormatException;
033: import java.awt.image.RenderedImage;
034: import java.awt.image.SampleModel;
035: import java.awt.image.TileObserver;
036: import java.awt.image.WritableRaster;
037: import java.awt.image.WritableRenderedImage;
038: import java.util.Arrays;
039: import java.util.Map;
040: import java.util.Vector;
041: import java.util.logging.Level;
042: import java.util.logging.LogRecord;
043: import java.util.logging.Logger;
044:
045: // JAI dependencies
046: import javax.media.jai.ImageLayout;
047: import javax.media.jai.PlanarImage;
048: import javax.media.jai.TileComputationListener;
049: import javax.media.jai.TileRequest;
050: import javax.media.jai.TileScheduler;
051:
052: // Geotools dependencies
053: import org.geotools.resources.Utilities;
054: import org.geotools.resources.XArray;
055: import org.geotools.resources.i18n.Logging;
056: import org.geotools.resources.i18n.LoggingKeys;
057: import org.geotools.resources.image.ColorUtilities;
058: import org.geotools.util.WeakValueHashMap;
059:
060: /**
061: * A tiled image to be used by renderer when the actual image may take a while to compute. This
062: * image wraps an arbitrary {@linkplain RenderedImage rendered image}, which may (or may not) be
063: * some image expensive to compute. When a tile is requested (through a call to {@link #getTile}
064: * but the tile is not available in the wrapped image, then this class returns some default
065: * (usually black) tile and start the real tile computation in a background thread. When the
066: * actual tile is available, this class fire a {@link TileObserver#tileUpdate tileUpdate} event,
067: * thus given a chance to a renderer to repaint again this image with the new tiles.
068: * <p>
069: * Simple example of use:
070: *
071: * <blockquote><pre>
072: * public class Renderer extends JPanel implements TileObserver {
073: * private DeferredPlanarImage image;
074: *
075: * public Renderer(RenderedImage toPaint) {
076: * image = new DeferredPlanarImage(toPaint);
077: * image.addTileObserver(this);
078: * }
079: *
080: * public void tileUpdate(WritableRenderedImage source,
081: * int tileX, int tileY, boolean willBeWritable)
082: * {
083: * repaint();
084: * }
085: *
086: * public void paint(Graphics gr) {
087: * ((Graphics2D) gr).drawRenderedImage(image);
088: * }
089: * }
090: * </pre></blockquote>
091: *
092: * @since 2.3
093: * @source $URL: http://svn.geotools.org/geotools/tags/2.4.1/modules/library/coverage/src/main/java/org/geotools/image/DeferredPlanarImage.java $
094: * @version $Id: DeferredPlanarImage.java 27862 2007-11-12 19:51:19Z desruisseaux $
095: * @author Remi Eve
096: * @author Martin Desruisseaux
097: */
098: public final class DeferredPlanarImage extends PlanarImage implements
099: WritableRenderedImage, TileObserver, TileComputationListener {
100: /**
101: * The logger for information messages.
102: */
103: private static final Logger LOGGER = org.geotools.util.logging.Logging
104: .getLogger("org.geotools.image");
105:
106: /**
107: * The thickness (in pixels) of the box to draw around deferred tiles, or 0 for disabling
108: * this feature. Current implementation draw a box only for {@link DataBufferByte} with
109: * only one band.
110: */
111: private static final int BOX_THICKNESS = 2;
112:
113: /**
114: * An entry in the {@link #buffers} map. Contains a sample model and the sample value
115: * used for filling the empty {@link DataBuffer} (usually 0, unless the color model had
116: * a transparent pixel different from 0).
117: */
118: private static final class Entry {
119: /** The sample model. */
120: public final SampleModel model;
121: /** The fill value. */
122: public final int fill;
123: /** The box value. */
124: public final int box;
125:
126: /** Constructs a new entry. */
127: public Entry(final SampleModel model, final int fill,
128: final int box) {
129: this .model = model;
130: this .fill = fill;
131: this .box = box;
132: }
133:
134: /** Returns a hash code value for this entry. */
135: public int hashCode() {
136: return model.hashCode();
137: }
138:
139: /** Compares this entry with the specified object. */
140: public boolean equals(final Object object) {
141: if (object instanceof Entry) {
142: final Entry that = (Entry) object;
143: return model.equals(that.model) && fill == that.fill
144: && box == that.box;
145: }
146: return false;
147: }
148: }
149:
150: /**
151: * Empty {@link DataBuffer} for a set of {@link SampleModel}.
152: * Will be created only when first needed.
153: */
154: private static Map buffers;
155:
156: /**
157: * The maximum delay (in milliseconds) to wait for a tile with one million pixels (e.g.
158: * a 1000×1000 tile). The actual {@link #delay} will be shorter if the tiles are
159: * smaller; for example the delay is four time smaller for a 500×500 tile. When
160: * a requested tile is not yet available, the {@link #getTile} method will wait for a
161: * maximum of {@code DELAY} milliseconds in case the tile computation would be very
162: * fast. Set the delay to 0 in order to disable this feature.
163: */
164: private static final int DELAY = 500;
165:
166: /**
167: * The delay (in milliseconds) to wait for a tile. When a requested tile is not yet available,
168: * the {@link #getTile} method will wait for a maximum of {@code delay} milliseconds in
169: * case the tile computation would be very fast. Set the delay to 0 in order to disable this
170: * feature.
171: */
172: private final int delay;
173:
174: /**
175: * The source image.
176: */
177: private final PlanarImage image;
178:
179: /**
180: * The tile observers, or {@code null} if none.
181: */
182: private TileObserver[] observers;
183:
184: /**
185: * The {@link TileRequest}s for a given tile.
186: * Tile index are computed by {@link #getTileIndex}.
187: * This array will be constructed only when first needed.
188: */
189: private transient TileRequest[] requests;
190:
191: /**
192: * Tells if a tile is request is waiting.
193: * Tile index are computed by {@link #getTileIndex}.
194: * This array will be constructed only when first needed.
195: */
196: private transient boolean[] waitings;
197:
198: /**
199: * Tells if a tile is in process of being computed.
200: * Tile index are computed by {@link #getTileIndex}.
201: * This array will be constructed only when first needed.
202: */
203: private transient Raster[] pendings;
204:
205: /**
206: * Constructs a new instance of {@code DeferredPlanarImage}.
207: *
208: * @param source The source image.
209: */
210: public DeferredPlanarImage(final RenderedImage source) {
211: super (new ImageLayout(source), toVector(source), null);
212: image = getSourceImage(0);
213: image.addTileComputationListener(this );
214: if (image instanceof WritableRenderedImage) {
215: ((WritableRenderedImage) image).addTileObserver(this );
216: }
217: delay = (int) Math.min((((long) DELAY)
218: * (tileWidth * tileHeight) / 1000000), DELAY);
219: }
220:
221: /**
222: * Wraps the specified image in a vector.
223: *
224: * @todo Should be inlined in the constructor if only Sun was to fix RFE #4093999
225: * ("Relax constraint on placement of this()/super() call in constructors").
226: */
227: private static Vector toVector(final RenderedImage image) {
228: final Vector vector = new Vector(1);
229: vector.add(image);
230: return vector;
231: }
232:
233: /**
234: * Returns the indice in {@link #requests} and {@link #pendings} array for the given tile.
235: * The {@code x} index varies fastest.
236: */
237: private int getTileIndice(final int tileX, final int tileY) {
238: assert tileX >= getMinTileX() && tileX <= getMaxTileX() : tileX;
239: assert tileY >= getMinTileY() && tileY <= getMaxTileY() : tileY;
240: return (tileY - getMinTileY()) * getNumXTiles()
241: + (tileX - getMinTileX());
242: }
243:
244: /**
245: * Returns the specified tile, or a default one if the requested tile is not yet available.
246: * If the requested tile is not immediately available, then an empty tile is returned and
247: * a notification will be sent later through {@link TileObserver} when the real tile will
248: * be available.
249: *
250: * @param tileX Tile X index.
251: * @param tileY Tile Y index.
252: * @return The requested tile.
253: */
254: public synchronized Raster getTile(final int tileX, final int tileY) {
255: if (requests == null) {
256: requests = new TileRequest[getNumXTiles() * getNumYTiles()];
257: }
258: final int tileIndice = getTileIndice(tileX, tileY);
259: TileRequest request = requests[tileIndice];
260: if (request == null) {
261: request = image.queueTiles(new Point[] { new Point(tileX,
262: tileY) });
263: requests[tileIndice] = request;
264: }
265: switch (request.getTileStatus(tileX, tileY)) {
266: default: {
267: LOGGER.warning("Unknow tile status");
268: // Fall through
269: }
270: case TileRequest.TILE_STATUS_CANCELLED: // Fall through
271: case TileRequest.TILE_STATUS_FAILED: // Fall through
272: case TileRequest.TILE_STATUS_COMPUTED:
273: return image.getTile(tileX, tileY);
274: case TileRequest.TILE_STATUS_PENDING: // Fall through
275: case TileRequest.TILE_STATUS_PROCESSING:
276: break;
277: }
278: /*
279: * The tile is not yet available. A background thread should be computing it right
280: * now. Wait a little bit in case the tile computation is very fast. If we can get
281: * the tile in a very short time, it would be more efficient than invoking some
282: * 'repaint()' method later.
283: */
284: if (pendings != null) {
285: if (pendings[tileIndice] != null) {
286: return pendings[tileIndice];
287: }
288: }
289: if (delay != 0) {
290: if (waitings == null) {
291: waitings = new boolean[requests.length];
292: }
293: waitings[tileIndice] = true;
294: try {
295: wait(delay);
296: } catch (InterruptedException exception) {
297: // Somebody doesn't want to lets us sleep. Go back to work.
298: }
299: waitings[tileIndice] = false;
300: switch (request.getTileStatus(tileX, tileY)) {
301: default:
302: return image.getTile(tileX, tileY);
303: case TileRequest.TILE_STATUS_PENDING: // Fall through
304: case TileRequest.TILE_STATUS_PROCESSING:
305: break;
306: }
307: }
308: /*
309: * The tile is not yet available and seems to take a long time to compute.
310: * Flag that this tile will need to be repainted later and returns an empty tile.
311: */
312: if (LOGGER.isLoggable(Level.FINER)) {
313: final LogRecord record = Logging.format(Level.FINER,
314: LoggingKeys.DEFERRED_TILE_PAINTING_$2, new Integer(
315: tileX), new Integer(tileY));
316: record.setSourceClassName(DeferredPlanarImage.class
317: .getName());
318: record.setSourceMethodName("getTile");
319: LOGGER.log(record);
320: }
321: if (pendings == null) {
322: pendings = new Raster[requests.length];
323: }
324: final Point origin = new Point(tileXToX(tileX), tileYToY(tileY));
325: final DataBuffer buffer = getDefaultDataBuffer(sampleModel,
326: colorModel);
327: final Raster raster = Raster.createRaster(sampleModel, buffer,
328: origin);
329: pendings[tileIndice] = raster;
330: fireTileUpdate(tileX, tileY, true);
331: return raster;
332: }
333:
334: /**
335: * Returns a databuffer for the specified sample model. If the image use an
336: * {@link IndexColorModel} and a {@linkplain IndexColorModel#getTransparentPixel
337: * transparent pixel} is defined, then raster sample values are initilized to
338: * the transparent pixel.
339: */
340: private static synchronized DataBuffer getDefaultDataBuffer(
341: final SampleModel sampleModel, final ColorModel colorModel) {
342: int fill = 0;
343: int box = 0;
344: if (colorModel instanceof IndexColorModel) {
345: final IndexColorModel colors = (IndexColorModel) colorModel;
346: fill = ColorUtilities.getTransparentPixel(colors);
347: if (BOX_THICKNESS > 0
348: && Math.min(sampleModel.getWidth(), sampleModel
349: .getHeight()) >= 64) {
350: box = ColorUtilities.getColorIndex(colors,
351: Color.DARK_GRAY, fill);
352: } else {
353: // Avoid drawing the box if tiles are too small.
354: box = fill;
355: }
356: }
357: final Entry entry = new Entry(sampleModel, fill, box);
358: if (buffers == null) {
359: buffers = new WeakValueHashMap();
360: }
361: DataBuffer buffer = (DataBuffer) buffers.get(entry);
362: if (buffer != null) {
363: return buffer;
364: }
365: /*
366: * No suitable data buffer existed prior to this call. Create a new one and fill it
367: * with the transparent color. Note that no filling is needed if the transparent value
368: * is 0, since the data buffer is already initialized to 0.
369: */
370: buffer = sampleModel.createDataBuffer();
371: if (fill > 0) {
372: for (int bank = buffer.getNumBanks(); --bank >= 0;) {
373: fill(buffer, bank, fill);
374: }
375: }
376: /*
377: * Draw a box around the tile. This is just a visual clue about tile location.
378: * Current implementation draw a box only for type byte with a single band.
379: */
380: if (BOX_THICKNESS > 0 && box != fill) {
381: if (sampleModel.getNumBands() == 1) {
382: final int width = sampleModel.getWidth();
383: int thickness = BOX_THICKNESS;
384: int offset = (width + 1) * thickness;
385: switch (buffer.getDataType()) {
386: case DataBuffer.TYPE_BYTE: {
387: final byte[] array = ((DataBufferByte) buffer)
388: .getData(0);
389: Arrays.fill(array, 0, offset, (byte) box);
390: Arrays.fill(array, array.length - offset,
391: array.length, (byte) box);
392: thickness *= 2;
393: while ((offset += width) < array.length) {
394: Arrays.fill(array, offset - thickness, offset,
395: (byte) box);
396: }
397: break;
398: }
399: }
400: }
401: }
402: buffers.put(entry, buffer);
403: return buffer;
404: }
405:
406: /**
407: * Sets all values in the specified bank to the specified value.
408: *
409: * @param buffer The databuffer in which to set all sample values.
410: * @param bank Index of the bank to set.
411: * @param value The value.
412: */
413: private static void fill(final DataBuffer buffer, final int bank,
414: final int value) {
415: switch (buffer.getDataType()) {
416: case DataBuffer.TYPE_BYTE:
417: Arrays.fill(((DataBufferByte) buffer).getData(bank),
418: (byte) value);
419: break;
420: case DataBuffer.TYPE_SHORT:
421: Arrays.fill(((DataBufferShort) buffer).getData(bank),
422: (short) value);
423: break;
424: case DataBuffer.TYPE_USHORT:
425: Arrays.fill(((DataBufferUShort) buffer).getData(bank),
426: (short) value);
427: break;
428: case DataBuffer.TYPE_INT:
429: Arrays.fill(((DataBufferInt) buffer).getData(bank),
430: (int) value);
431: break;
432: case DataBuffer.TYPE_FLOAT:
433: Arrays.fill(((DataBufferFloat) buffer).getData(bank),
434: (float) value);
435: break;
436: case DataBuffer.TYPE_DOUBLE:
437: Arrays.fill(((DataBufferDouble) buffer).getData(bank),
438: (double) value);
439: break;
440: default:
441: throw new RasterFormatException(String.valueOf(buffer));
442: }
443: }
444:
445: /**
446: * A tile is about to be updated (it is either about to be grabbed for writing,
447: * or it is being released from writing).
448: */
449: private void fireTileUpdate(final int tileX, final int tileY,
450: final boolean willBeWritable) {
451: final TileObserver[] observers = this .observers; // Avoid the need for synchronisation.
452: if (observers != null) {
453: final int length = observers.length;
454: for (int i = 0; i < length; i++) {
455: try {
456: observers[i].tileUpdate(this , tileX, tileY,
457: willBeWritable);
458: } catch (RuntimeException cause) {
459: /*
460: * An exception occured in the user code. Unfortunatly, we are probably not in
461: * the mean user thread (e.g. the Swing thread). This method is often invoked
462: * from some JAI's worker thread, which we don't want to corrupt. Log a warning
463: * for the user and lets the JAI's worker thread continue its work.
464: */
465: String message = cause.getLocalizedMessage();
466: if (message == null) {
467: message = Utilities.getShortClassName(cause);
468: }
469: final LogRecord record = new LogRecord(
470: Level.WARNING, message);
471: record.setSourceClassName(observers[i].getClass()
472: .getName());
473: record.setSourceMethodName("tileUpdate");
474: record.setThrown(cause);
475: LOGGER.log(record);
476: }
477: }
478: }
479: }
480:
481: /**
482: * Invoked when a tile has been computed.
483: *
484: * @param eventSource The caller of this method.
485: * @param requests The relevant tile computation requests as returned by the method
486: * used to queue the tile.
487: * @param image The image for which tiles are being computed as specified to the
488: * {@link TileScheduler}.
489: * @param tileX The X index of the tile in the tile array.
490: * @param tileY The Y index of the tile in the tile array.
491: * @param tile The computed tile.
492: */
493: public void tileComputed(final Object eventSource,
494: final TileRequest[] requests, final PlanarImage image,
495: final int tileX, final int tileY, final Raster tile) {
496: synchronized (this ) {
497: final int tileIndice = getTileIndice(tileX, tileY);
498: if (waitings != null && waitings[tileIndice]) {
499: /*
500: * Notify the 'getTile(...)' method in only ONE thread that a tile is available.
501: * If tiles computation occurs in two or more background thread, then there is no
502: * garantee that the notified thread is really the one waiting for this particular
503: * tile. However, this is not a damageable problem; the delay hint may just not be
504: * accuratly respected (the actual delay may be shorter for wrongly notified tile).
505: */
506: notify();
507: }
508: if (pendings == null || pendings[tileIndice] == null) {
509: return;
510: }
511: pendings[tileIndice] = null;
512: }
513: fireTileUpdate(tileX, tileY, false);
514: }
515:
516: /**
517: * Invoked when a tile computation has been cancelled. The default implementation does nothing.
518: */
519: public void tileCancelled(final Object eventSource,
520: final TileRequest[] requests, final PlanarImage image,
521: final int tileX, final int tileY) {
522: }
523:
524: /**
525: * Invoked when a tile computation failed. Default implementation log a warning and lets the
526: * program continue as usual. We are not throwing an exception since this failure will alter
527: * the visual rendering, but will not otherwise harm the system.
528: */
529: public void tileComputationFailure(final Object eventSource,
530: final TileRequest[] requests, final PlanarImage image,
531: final int tileX, final int tileY, final Throwable cause) {
532: final LogRecord record = new LogRecord(Level.WARNING, cause
533: .getLocalizedMessage());
534: record.setSourceClassName(DeferredPlanarImage.class.getName());
535: record.setSourceMethodName("getTile");
536: record.setThrown(cause);
537: LOGGER.log(record);
538: }
539:
540: /**
541: * Invoked if the underlying image is writable and one of its tile changed.
542: * This method forward the call to every registered listener.
543: */
544: public void tileUpdate(final WritableRenderedImage source,
545: final int tileX, final int tileY,
546: final boolean willBeWritable) {
547: fireTileUpdate(tileX, tileY, willBeWritable);
548: }
549:
550: /**
551: * Adds an observer. This observer will be notified everytime a tile initially empty become
552: * available. If the observer is already present, it will receive multiple notifications.
553: */
554: public synchronized void addTileObserver(final TileObserver observer) {
555: if (observer != null) {
556: if (observers == null) {
557: observers = new TileObserver[] { observer };
558: } else {
559: final int length = observers.length;
560: observers = (TileObserver[]) XArray.resize(observers,
561: length + 1);
562: observers[length] = observer;
563: }
564: }
565: }
566:
567: /**
568: * Removes an observer. If the observer was not registered, nothing happens.
569: * If the observer was registered for multiple notifications, it will now be
570: * registered for one fewer.
571: */
572: public synchronized void removeTileObserver(
573: final TileObserver observer) {
574: if (observers != null) {
575: for (int i = observers.length; --i >= 0;) {
576: if (observers[i] == observer) {
577: observers = (TileObserver[]) XArray.remove(
578: observers, i, 1);
579: break;
580: }
581: }
582: }
583: }
584:
585: /**
586: * Checks out a tile for writing. Since {@code DeferredPlanarImage} are not really
587: * writable, this method throws an {@link UnsupportedOperationException}.
588: */
589: public WritableRaster getWritableTile(final int tileX,
590: final int tileY) {
591: throw new UnsupportedOperationException();
592: }
593:
594: /**
595: * Relinquishes the right to write to a tile. Since {@code DeferredPlanarImage} are
596: * not really writable, this method throws an {@link IllegalStateException} (the state is
597: * really illegal since {@link #getWritableTile} should never have succeeded).
598: */
599: public void releaseWritableTile(final int tileX, final int tileY) {
600: throw new IllegalStateException();
601: }
602:
603: /**
604: * Returns whether any tile is checked out for writing.
605: */
606: public boolean hasTileWriters() {
607: final Raster[] pendings = this .pendings; // Avoid the need for synchronisation.
608: if (pendings != null) {
609: final int length = pendings.length;
610: for (int i = 0; i < length; i++) {
611: if (pendings[i] != null) {
612: return true;
613: }
614: }
615: }
616: return false;
617: }
618:
619: /**
620: * Returns whether a tile is currently checked out for writing.
621: */
622: public boolean isTileWritable(final int tileX, final int tileY) {
623: final Raster[] pendings = this .pendings; // Avoid the need for synchronisation.
624: return pendings != null
625: && pendings[getTileIndice(tileX, tileY)] != null;
626: }
627:
628: /**
629: * Returns an array of {@link Point} objects indicating which tiles are
630: * checked out for writing. Returns null if none are checked out.
631: */
632: public synchronized Point[] getWritableTileIndices() {
633: Point[] indices = null;
634: if (pendings != null) {
635: int count = 0;
636: final int minX = getMinTileX();
637: final int minY = getMinTileY();
638: final int numX = getNumXTiles();
639: final int length = pendings.length;
640: for (int i = 0; i < length; i++) {
641: if (pendings[i] != null) {
642: if (indices == null) {
643: indices = new Point[length - i];
644: }
645: final int x = i % numX + minX;
646: final int y = i / numX + minY;
647: assert getTileIndice(x, y) == i : i;
648: indices[count++] = new Point(x, y);
649: }
650: }
651: if (indices != null) {
652: indices = (Point[]) XArray.resize(indices, count);
653: }
654: }
655: return indices;
656: }
657:
658: /**
659: * Sets a rectangle of the image to the contents of the raster. Since
660: * {@code DeferredPlanarImage} are not really writable, this method
661: * throws an {@link UnsupportedOperationException}.
662: */
663: public void setData(Raster r) {
664: throw new UnsupportedOperationException();
665: }
666:
667: /**
668: * Provides a hint that this image will no longer be accessed from a reference in user space.
669: * <strong>NOTE: this method dispose the image given to the constructor as well.</strong>
670: * This is because {@code DeferredPlanarImage} is used as a "view" of an other
671: * image, and the user shouldn't know that he is not using directly the other image.
672: */
673: public synchronized void dispose() {
674: if (image instanceof WritableRenderedImage) {
675: ((WritableRenderedImage) image).removeTileObserver(this);
676: }
677: image.removeTileComputationListener(this);
678: requests = null;
679: waitings = null;
680: pendings = null;
681: super.dispose();
682: image.dispose();
683: }
684: }
|