/*
* Copyright 2003-2010 Tufts University Licensed under the
* Educational Community License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License. You may
* obtain a copy of the License at
*
* http://www.osedu.org/licenses/ECL-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an "AS IS"
* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing
* permissions and limitations under the License.
*/
package tufts.vue;
import tufts.Util;
import java.lang.ref.*;
import java.awt.Image;
import java.awt.Graphics2D;
import java.awt.geom.Rectangle2D;
import java.awt.geom.AffineTransform;
import static tufts.vue.ImageRep.UNAVAILABLE;
public class ImageRef
{
public static final int DEFAULT_ICON_SIZE = 128;
public static final int[] ZERO_SIZE = ImageRep.ZERO_SIZE;
public static final Object GOT_SIZE = "ImageRef.GOT-SIZE";
public static final Object REPAINT = "ImageRef.REPAINT";
public static final Object KICKED = "ImageRef.*****KICKED*****";
public static final ImageRef EMPTY = new ImageRef() {
// @Override public void setImageSource(Object is) {
// Log.error("attempt to set image source on the empty ImageRef: " + Util.tags(is), new Throwable("HERE"));
// }
@Override protected void repaint() {}
@Override protected void ensureLoading(ImageRep rep, boolean lowPriorityCache) {}
@Override void preCacheRef() {}
@Override public boolean equals(Object o) { return false; }
@Override public String toString() { return "ImageRef[___EMPTY___]"; }
};
//===================================================================================================
private static final org.apache.log4j.Logger Log = org.apache.log4j.Logger.getLogger(ImageRef.class);
private static final int PIXEL_THRESHOLD_FOR_ICON_GENERATION = (2*DEFAULT_ICON_SIZE) * (2*DEFAULT_ICON_SIZE);
/**
* This determins when icons are drawn v.s. the full rep. The smaller this is, the more
* often the full representation will be requested for drawing.
*/
private static final int PIXEL_THRESHOLD_FOR_ICON_DRAWING = DEFAULT_ICON_SIZE*2;
private static final boolean ICONS_ARE_DISPOSABLE = false; // todo: true case needs testing / may not work
private final ImageSource _source;
private final Listener _repainter;
private volatile float _aspect = 0;
private volatile ImageRep _full = ImageRep.UNAVAILABLE;
private volatile ImageRep _icon = ImageRep.UNAVAILABLE;
//private volatile Object _desired = SIZE_UNKNOWN;
// _desired not used at moment -- would be easy to have one global instance of an ImageRef per image w/out it,
// and could add back in this functionality by allowing a client to implement a simple recording API for desired
// e.g., set/getDesired -- is only needed to prevent extra repaints when a new image rep arrives.
public static interface Listener {
public void imageRefUpdate(Object cause);
}
// static ImageRef create(java.io.File file) {
// return null;
// }
public static ImageRef create(Listener listener, Object imageData) {
// could allow for single-instance per Resource/URI caching here
return new ImageRef(listener, imageData);
}
private ImageRef(Listener listener, Object imageData) {
_repainter = listener;
_source = ImageSource.create(imageData);
//if (DEBUG.IMAGE) Log.debug("created image source " + _source + " from " + is);
initReps();
}
private ImageRef() {
_source = null;
_repainter = null;
}
ImageSource source() {
return _source;
}
// public boolean isBlank() {
// return _source == null;
// }
// private void setImageSource(Object is) {
// //if (_source != null) throw new Error("ImageSource re-set not permitted: " + this);
// if (_source != is) {
// //-----------------------------------------------------------------------------
// // PROBLEM: if this is a local file, the URI cache key in the _source.key
// // will be null, meaning we can't later create an icon cache key from it.
// // Yet at the moment we're only seeing this as a problem if the local
// // file is missing -- so how is this working in the regular case?
// //-----------------------------------------------------------------------------
// _source = ImageSource.create(is);
// //if (DEBUG.IMAGE) Log.debug("created image source " + _source + " from " + is);
// initReps();
// }
// }
private static final boolean PRE_LOAD_ICONS = false;
private void initReps() {
if (DEBUG.IMAGE) debug("initReps");
//boolean reload = false;
// if (_icon != UNAVAILABLE || _full != UNAVAILABLE) {
// //Log.info("re-loading ref " + ref);
// throw new Error("re-init of reps");
// }
// rep won't load until it attempts to draw:
// We init this first so it's available for the icon to init with
// the full pixel original size pulled from the icon.
_full = ImageRep.create(this, _source);
// As icons only exist in the cache, we know we don't have an icon
// created yet if there isn't at least a unloaded cache entry for the icon.
final java.net.URI iconKey = _source.getIconKey(DEFAULT_ICON_SIZE);
if (iconKey != null) {
if (PRE_LOAD_ICONS) {
//synchronized (Images.getCacheLock()) {
// we did this in a cache-lock to ensure the cache entry can't have been GC'd or
// changed from the time we check for it to the time we kick a load for it,
// but could we dead-lock? -- Ideally, we want to release the lock at the point
// getImage returns in ImageRep.reconstitute, otherwise it's ImageRef callback,
// notifyRepHasArrived, will also happen in the cache-lock, which is dangerous
// -- it will block all other image processing threads till it returns [todo:
// test/verify]
if (Images.hasCacheEntry(iconKey)) {
_icon = createPreLoadedIconRep(iconKey);
ensureLoading(_icon);
}
//}
} else {
if (Images.hasCacheEntry(iconKey)) {
_icon = createPreLoadedIconRep(iconKey);
}
}
} // _icon left as ImageRep.UNAVAILABLE
// // rep won't load until it attempts to draw:
// _full = ImageRep.create(this, _source);
}
// public void drawInto(Graphics2D g, float width, float height)
// {
// // need to always try, as this is how we'll know if the
// // the full is ever desired (when an icon has been pre-loaded)
// drawBestAvailable(g, width, height);
// }
public void drawInto(DrawContext dc, float width, float height)
{
try {
drawBestAvailable(dc, width, height);
} catch (Throwable t) {
Log.error("exception painting " + this, t);
}
}
// private void drawAvailable(Graphics2D g, float width, float height)
// {
// if (_icon.available())
// _icon.renderRep(g, width, height);
// else if (_full.available())
// _full.renderRep(g, width, height);
// else
// ; //UNAVAILABLE.drawRep(g, width, height);
// }
private static final java.awt.Color LoadingOverlay = new java.awt.Color(128,128,128,128);
private static final java.awt.Color LoadingOverlayWhite = new java.awt.Color(255,255,255,128);
private static final java.awt.Color LoadingOverlayBlack = new java.awt.Color(0,0,0,128);
private static final java.awt.Color DebugRed = new java.awt.Color(255,0,0,128);
private static final java.awt.Color DebugGreen = new java.awt.Color(0,255,0,128);
private static final java.awt.Color DebugBlue = new java.awt.Color(0,0,255,128);
private static final java.awt.Color DebugYellow = new java.awt.Color(255,255,0,128);
private ImageRep pickRepToDraw(final ImageRep ideal) {
return pickRepToDraw(ideal, ideal == _full ? _icon : _full);
}
private ImageRep pickRepToDraw(final ImageRep desired, final ImageRep backup)
{
// Note that a rep that is not "available" may also be "loading", and be receiving progress
// on that load which it is able to display. (Maybe different semantics would be helpful
// e.g. -- provide a "drawable" as well as "available" and/or rename "available" to
// "loaded").
if (desired.available()) // the most common case at runtime
return desired;
else if (backup.available())
return backup;
else if (desired == backup) {
// this is the common case at init
if (desired != ImageRep.UNAVAILABLE) {
// should never happen
Log.error("desired == backup != UNAVAILABLE: " + desired, new Throwable("HERE"));
reload();
}
return ImageRep.UNAVAILABLE;
}
else if (desired.loading())
return desired;
else if (backup.loading())
return backup;
else if (desired.hasError())
return desired;
else if (backup.hasError())
return backup;
else
return ImageRep.UNAVAILABLE;
}
//private static final double ASSUMED_PRINTER_DPI = 600;
private ImageRep getIdealRep(DrawContext dc, float width, float height, Image[] GC_lock)
{
if (dc.isPrintQuality()) {
// Note: Even the non-blocking image fetch runes in ImageRep w/out a listener, ImageRep
// still generates needed callbacks to us as this comes in via cacheData. It's
// possibly we may at some point want to skip even that though, as it could place less
// strain on memory if we're running low. Memory constraints while printing appear to
// be worse than just for rendering maps to the screen. E.g., we would never want to
// start generating an icon during a print job, tho that is theoretically possible. So,
// todo: at some point ensure that ImageRep.cacheData w/immediate call does NOT make
// the ImageRef callback, and we do anything we need to do for recording an arrived
// full rep right here, immediately (e.g., record the size if this is the first time
// we've seen the full rep). So ImageRef may want it's own cacheData style method to
// serve the same purpose it does in ImageRep.
GC_lock[0] = _full.getImageBlocking();
return _full;
}
// final double scale;
// // Actually, little point in trying this optimization -- e.g., if
// // we're "printing" to a PDF or a print-preview, it's generated with essentially infinte scale,
// // as the result will be user-zoomable. [HOWEVER: we may wish to restore this just to reduce memory consumption]
// if (dc.isPrintQuality()) {
// scale = dc.g.getTransform().getScaleX() * (ASSUMED_PRINTER_DPI / 72.0);
// } else {
// scale = dc.g.getTransform().getScaleX();
// }
final double scale = dc.g.getTransform().getScaleX();
final int onDisplayMaxDim;
if (aspect() > 1f) {
//debug("aspect="+aspect() + " picking width " + width);
onDisplayMaxDim = (int) (scale * width);
} else {
//debug("aspect="+aspect() + " picking height " + height);
onDisplayMaxDim = (int) (scale * height);
}
final ImageRep idealRep;
// We don't worry about coherency sync issues between _full & _icon here -- we should
// handle whatever's thrown at us on a best-available basis. The only thing we rely on is
// they should never be null.
if (onDisplayMaxDim <= PIXEL_THRESHOLD_FOR_ICON_DRAWING) {
// It would be more complete to check the actual iconRep size v.s. our constant, tho this
// lets us not worry if it's been loaded or not, and currently we only ever generate
// icons of a single size. Note that this also assumes the icon will always be smaller
// than the full rep, which should hold true as we don't bother to generate an icon
// otherwise.
idealRep = _icon;
//backupRep = _full;
//_desired = SIZE_ICON;
//debug("onScreenMaxDim below thresh " + PIXEL_THRESHOLD_FOR_ICON_DRAWING + " at " + onScreenMaxDim);
} else {
//debug("onScreenMaxDim ABOVE thresh " + PIXEL_THRESHOLD_FOR_ICON_DRAWING + " at " + onScreenMaxDim);
//_desired = SIZE_FULL;
idealRep = _full;
//backupRep = _icon;
}
// Technically, we only need a GC lock for _full, as _icon reps are permanently GC locked internally
// in the current implementation.
GC_lock[0] = idealRep.image();
// if (dc.isPrintQuality()) {
// Log.debug(String.format("print GC scale %.2f net=%.2f px=%4d %s",
// dc.g.getTransform().getScaleX(), scale, onDisplayMaxDim, idealRep));
// }
return idealRep;
}
private void drawBestAvailable(DrawContext dc, float width, float height)
{
// Tasks to accomplish here:
// (1) find the ideal representation for the situation (the current rendering size v.s. available pixels)
// (2) pick the best representation to draw given what's actually available
// (3) start loading the ideal represation for future use if it wasn't available
final Image[] idealImageLock = new Image[1]; // for holding a GC-lock on the image if it's there
final ImageRep ideal = getIdealRep(dc, width, height, idealImageLock);
final ImageRep drawable;
if (idealImageLock[0] != null) {
// This is the common case once everything has been loaded and cached in memory, and presuming
// we're not running low on memory.
renderImage(dc.g, idealImageLock[0], width, height);
idealImageLock[0] = null; // ensure GC-lock is immediately released
// setting drawable here is now just for possible debug, which could probably be factored out,
// and allow us to return an object from getIdealRep, which is sometimes an Image, sometimes
// an ImageRep.
drawable = ideal;
} else {
// Note: the logic below is tuned to cover many possible corner cases,
// and is not easy to refactor w/out breaking one or more of them.
drawable = pickRepToDraw(ideal);
if (DEBUG.IMAGE && DEBUG.BOXES) {
debug(" ideal " + ideal);
debug(" drawable " + drawable);
}
if (!dc.isAnimating()) {
// We never kick image data loading during animations, as the desired representation
// may only be a momentary need (and it could suddenly slow down the animation to boot).
// Note: ideal may already be loading -- kickLoad handles all that.
ensureLoading(ideal, drawable);
}
// rendering before/after kickloads doesn't matter as long as reps don't auto-constitute
drawable.renderRep(dc.g, width, height);
if (DEBUG.Enabled) {
if (drawable != ideal && !dc.isPrintQuality() && drawable.available() && !ideal.hasError())
drawBetterRepAvailableIndicator(dc.g, width, height);
}
}
if (DEBUG.BOXES) drawDebugStatus(dc.g, ideal, drawable, width, height);
}
/** render a fully loaded image who's size is known to the java.awt.Image provided into the given width/height */
static void renderImage
(final Graphics2D g,
Image image,
final float toWidth,
final float toHeight)
{
final float pixelsWide = image.getWidth(null);
final float pixelsTall = image.getHeight(null);
g.drawImage(image,
AffineTransform.getScaleInstance(toWidth / pixelsWide,
toHeight / pixelsTall),
null);
image = null; // attempt to help GC
// todo performance: keep a re-usable AffineTransform in the DrawContext for the above
// kinds of usage, and just use setToScale on it? Add image rendering to the DrawContext?
}
static void drawBetterRepAvailableIndicator(Graphics2D g, float width, float height) {
// draw a "loading" indicator
// if (drawable != ideal)
// dc.g.setColor(DebugRed);
// else
// dc.g.setColor(DebugGreen);
final float sw = Math.max(width,height) / 64f;
g.setStroke(new java.awt.BasicStroke(sw));
final float xoff, yoff;
xoff = yoff = sw / 2f + 0.5f;
//xoff = width / 8f;
//yoff = height / 8f;
final Rectangle2D.Float r = new Rectangle2D.Float(xoff,yoff,width-xoff*2,height-yoff*2);
//final Rectangle2D.Float r = new Rectangle2D.Float(xoff,yoff,width-sw,height-sw);
g.setColor(LoadingOverlayWhite);
g.draw(r);
g.setColor(LoadingOverlayBlack);
r.x += xoff;
r.y += yoff;
r.width -= xoff * 2;
r.height -= yoff * 2;
g.draw(r);
}
private void drawDebugStatus(Graphics2D g, ImageRep idealRep, ImageRep drawRep, float width, float height)
{
final float hw = width / 2f;
final float hh = height / 2f;
final java.awt.geom.Rectangle2D.Float r = new java.awt.geom.Rectangle2D.Float();
if (drawRep == _icon) {
// we're looking at the icon rep
g.setColor(DebugYellow);
r.setRect(0, 0, hw, hh);
g.fill(r);
}
if (drawRep != idealRep) {
// we're waiting for a better rep
g.setColor(DebugRed);
r.setRect(0, hh, hw, hh);
g.fill(r);
}
if (_full.available()) {
// // we've got the full rep loaded
// if (_full.isFading()) // was to check to Reference enequing
// g.setColor(DebugYellow);
// else
g.setColor(DebugBlue);
r.setRect(hw, 0, hw, height);
g.fill(r);
}
}
protected void repaint() {
_repainter.imageRefUpdate(REPAINT);
}
private static final boolean ENABLE_IMMEDIATE_SIZES = true; // turn off for debugging undo of delayed size reports/layouts
public void notifyRepHasProgress(final ImageRep rep, final float pct) {
if (ENABLE_IMMEDIATE_SIZES && _aspect == 0 && rep == _full && rep.size() != ZERO_SIZE) {
_aspect = rep.aspect();
_repainter.imageRefUpdate(GOT_SIZE);
} else {
repaint();
}
}
/** the ImageRep is done loading -- it has all the renderable image data, unless hardImageRef is null,
in which case we had an error */
public void notifyRepHasArrived(final ImageRep freshRep, final Images.Handle hardImageRef)
{
//if (_desired == freshRep || _desired == SIZE_UNKNOWN) // may be easiest/safest just to always repaint
// used to force some reasonable aspect at top and always issue the repaint first
//repaint();
// note that we may actually get this call with hardImageRef set to null, which means we got an error,
// and just want to repaint
if (freshRep == _full && _icon == UNAVAILABLE && hardImageRef != null) {
// no icon was previously generated -- look to see if
// one has been generated elsewhere in this runtime,
// or if not, and we need one, create it now.
if (_full.area() > PIXEL_THRESHOLD_FOR_ICON_GENERATION) {
_icon = createRuntimeScaledIconRep(freshRep, hardImageRef.image);
} // else _icon left as ImageRep.UNAVAILABLE
}
else if (freshRep == _icon && (_full == UNAVAILABLE || _full.size() == ZERO_SIZE)) {
// We don't have a full rep loaded -- pull it from meta-data stored with the icon.
// There are several reasons this is important: (1) We may need to know the full
// pixel size before the full representation is available (e.g., set to natural size).
// (2) We want to know our "perfect" aspect even if we don't have the full image.
// Generated icons may have slight changes in aspect, and even slight changes in aspect
// has given us major problems in the past (e.g., maps w/images changing the first time
// they're opened). (3) an interaction of these various problems had completey broken
// image folder import.
loadFullPixelSize(hardImageRef);
}
if (_aspect == 0 || freshRep == _full) {
// note: this used to be at the very top, before the repaint() issue
_aspect = freshRep.aspect();
}
repaint();
}
private void loadFullPixelSize(Images.Handle icon) {
try {
unpackFullPixelSize(icon);
} catch (Throwable t) {
Log.error("extracting full pixel size from " + icon, t);
}
}
private void unpackFullPixelSize(Images.Handle icon) {
Object ss = icon.data.get("sourcePixels");
if (ss != null) {
if (_full == UNAVAILABLE) {
Log.error("UNAVAILABLE FULL-REP", new Throwable("HERE"));
// note: we could lazily force create the _full rep with just the size info,
// but currently reps should have always already been created at init (containing
// no data at all except their ImageSource)
//_full = ImageRep.create(this, _source);
} else {
//if (DEBUG.Enabled) Util.dump(icon.data, "FOUND SOURCE PIXEL SIZE");
_full.takeSize((int[])ss);
_aspect = _full.aspect(); // force aspect based on exact pixel dimensions
}
}
//========================================================================================
// This problem is what determined that we MUST save this size in the cache somehow (e.g.,
// with the icon). PROBLEM: if an image has an icon in cache, and we're creating a NEW
// RESOURCE, such that resource properties image.width & image.height were never set, we
// can't know the full pixel size. Well HAVE to use the aspect (old image code didn't use
// aspect here -- always used full pixel size). The ONLY WAY around that one, w/out
// forcing a load of a the whole image (which defeats the purpose of the image code
// entirely) would be to store the full pixel size in the icon itself somehow. That would
// be a good idea anyway... how to best do it? .PNG meta-data would be great, tho putting
// it in the filename would be easier, tho if if the source image changed... actually,
// that could be one way we detect that the source image has changed, tho including the
// modification date would be ideal -- now we REALLY need meta-data... Oh, wait, we could
// actually use the modification date of the icon file -- just make sure it's AFTER the
// on-disk file.
// ========================================================================================
}
private ImageRep createPreLoadedIconRep(java.net.URI cacheKey)
{
return ImageRep.create(this,
ImageSource.create(cacheKey),
ICONS_ARE_DISPOSABLE);
}
private ImageRep createRuntimeScaledIconRep(final ImageRep full, final Image hardFullImageRef) {
// could pass in something like Scaler with just a produceIcon method into the image source
// for creating the icon, or a general FutureTask.
// NOTE: if the icon is NOT created immediately, and we're in the middle of loading lots of
// images, that hard image reference is going to stay around, held by the ImageSource in an
// Images IconTask, in the the thread-pool task queue, unable to be GC'd, which will lead
// to contention that's very difficult to recover from should we start running out of
// memory.
// Although an incompletely impl, Images.DELAYED_ICONS uses the full ImageRep into the
// icon-source ImageSource instead of the hard image reference, and could attempt to
// reconstitute it if it's been GC'd once the IconTask get's around to running. That
// implies a bunch more complexity to the code. We're going with simpler and more reliable
// for now.
final ImageRep icon = ImageRep.create(this,
ImageSource.createIconSource(_source, full, hardFullImageRef, DEFAULT_ICON_SIZE),
ICONS_ARE_DISPOSABLE);
// TODO: if the source image changes on disk, any icon needs to be re-generated
// For ideal memory usage, we'd create the icon immediately in this thread, but we don't
// actually want to do this: if there are multiple Ref's to the same content, they'll all
// be in the listener-relay chain, but the FIRST one to get this callback is going to hang
// up the rest of the thread while generating the icon if we request an immediate load, and
// the down-relay ImageRef's, which could at least draw the full-rep while waiting, will be
// waiting until the icon generation is done. Furthermore: this will trigger callbacks
// with icon data to nodes, THEN the backed up relay's will fire, making it look like the
// full rep has arrived after the icon, which explains why when we tried this some of the
// images in our repeats test are displaying the full image AFTER it's been generated, tho
// that gets fixed on the first repaint after the updates.
// The issue of generating icons sooner rather than later is how handled
// in Images by giving higher priority to icon generating tasks than image loading tasks,
// and by keeping a hard-ref to the image in the ImageSource.
ensureLoading(icon);
return icon;
}
private void ensureLoading(ImageRep ideal, ImageRep drawable)
{
// Note: the logic below is tuned to cover many possible corner cases,
// and is not easy to refactor w/out breaking one or more of them.
if (ideal != UNAVAILABLE) {
ensureLoading(ideal);
} else if (!drawable.available()) {
if (drawable == UNAVAILABLE) { // if icon load failed, must create a new one (low memory) [NOT ENOUGH!]
if (DEBUG.IMAGE||DEBUG.WORK) debug("forcing full load");
ensureLoading(_full);
} else if (drawable == _icon && drawable.hasError()) {
//****************************************************************************************
// if icon load failed, must create a new one (low memory) [TODO: NOT ENOUGH]
//****************************************************************************************
if (DEBUG.Enabled) debug("forcing full load on bad icon");
ensureLoading(_full);
} else
ensureLoading(drawable);
}
}
private void ensureLoading(ImageRep rep) {
ensureLoading(rep, false);
}
void preCacheRef() {
ensureLoading(_icon, true);
ensureLoading(_full, true);
}
protected void ensureLoading(ImageRep rep, boolean lowPriorityCache) {
if (rep == UNAVAILABLE || rep.loading() || rep.available()) {
// note: this is probably being called more often than need be
// Currently, ImageRep's handle being in first a CACHING state, and then
// upgrading to a LOADING state if they're later requested for a real paint.
// This allows us, for instance, to start caching all the items in a
// presentation, but then re-prioritize paint request if the user fast-pages
// through the presentation or jumps to the middle. If we ALSO want to
// support being able to fast-page through, then fast-page BACK over items
// that have already been given LOADING priority, and re-prioritize them to
// the front of the LIFO queue, we'd need to issue another Images call here
// to make that request. Generally, that should actually work fine, tho in
// rare cases where lots of high-res images are being requested at once, the
// queue will thrash a bit -- that is, be fully rotating on each paint.
// Note: this is also a point where we can see coherency issues arise with
// the internal Images cache. E.g., Images keeps a cache of images, or, if
// requested but not loaded, tasks to load those images. ImageRep maintains
// a state of request & result, which needs to be in agreement with the
// Images state. E.g., Images can't just punt items from it's task queue
// w/out doing something to notify the ImageRep about to udpate it's state,
// which determines if it thinks theres callbacks coming (e.g., the call to
// rep.loading() above). Making that issue go away is a greenfield operation
// that would likely entail keeping ImageRef's directly in the cache, so
// there's only ever one state, and it's maintained there.
return;
}
if (lowPriorityCache) {
//if (DEBUG.IMAGE) debug("->caching " + rep);
rep.requestCaching();
} else {
//if (DEBUG.IMAGE) debug(">kickLoad " + rep);
final boolean waitingForCallback = rep.reconstitute();
if (DEBUG.BOXES && waitingForCallback) { //&& rep == _full)
// we should only need this for debug -- to repaint the status
_repainter.imageRefUpdate(KICKED);
}
}
}
private void debug(String s) {
Log.debug(String.format("%08x[%s] %s", System.identityHashCode(this), debugSRC(_source), s));
}
static String debugSRC(ImageSource s)
{
if (s == null)
return "[null ImageSource]";
else
return s.debugName();
}
public boolean available() {
return _icon.available() || _full.available();
}
public boolean hasError() {
return _full.hasError();
}
public float aspect() {
return _aspect;
}
public int[] fullPixelSize() {
return _full.size();
}
public void reload() {
_full = ImageRep.UNAVAILABLE;
_icon = ImageRep.UNAVAILABLE;
repaint();
}
@Override public boolean equals(Object o) {
if (o instanceof ImageRef)
return ((ImageRef)o)._source.original == _source.original;
else
return false;
}
@Override public String toString() {
//return "ImageRef[full=" + fullRep() + "; icon=" + iconRep() + "; src=" + _source + "]";
return String.format("ImageRef[full=%s icon=%s]", _full, _icon.handle());
//return String.format("ImageRef[full: %s\n\ticon: %s\n\tsrc: %s]", _full, _icon, _source);
}
}
// ALTERNATIVE DESIGN: Images.getImageRef returns an ImageRef, so that multiple
// LWImage's, preview panes, etc, are all pointing to the same ImageRef. Which means
// the cache would always contain instances of ImageRef? Tho that conflicts with the
// idea of keeping the cache code very clean & flat: e.g., only knows about 1 key, 1
// file, and the the icon cache file generation would get handled in ImageRef. Tho
// actually that really doesn't need to be -- Images handles fetching and caching image
// data -- if it creates icons behind the scenes, that's an impl detail.
// If we instance the ImageRef ourselves, that is more amenable to subclassing and just
// overriding the update methods we'd like -- that could be very handy. That would work
// here and in ResourceIcon, both which always work with a single image. But
// PreviewPane works w/multiple images, so that means it needs the callbacks, tho the
// only once it really needs are gotImage and gotImageError.
// The big advantage to having the ImageRef doing the low-level listening to the image
// loader is that it can be one place that could handle a default common form of display
// update: e.g., while image is loading, draw as a transparent box w/loading %, display
// common unavail info, etc. It would also probably want to take a java.awt.Component
// handle for being able to issue optional repaint() calls. It would be one standard
// place to have all the info about raw size, loading status, etc. It could also handle
// going between a Resource (if it has one) and the image data/image icon.
//----------------------------------------------------------------------------------------
// Note: subclassing java.awt.Image is not actually supported; in AWT Graphics drawing:
// SurfaceManager can't get surface. see
// http://forums.sun.com/thread.jspa?threadID=5208043 -- basically, this is a 10+ year
// old bug that's never been fixed -- either java.awt.Image needs to be abstract or the
// impls need changing, and it looks like there's just no hope on this one at all.
// Subclassing BufferedImage MAY be possible tho... this would lock us in 100% to never
// using ToolkitImage's, tho we can probably live with that as that impl gives us memory
// heartburn, at least on the Mac. The bigger issue is we'd like to be able to rely on
// using createCompatibleImage for creating our BufferedImages, and not have to create
// our own delegating instances.