/* * Copyright 2003-2008 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.awt.Image; import java.awt.Point; import java.awt.Color; import java.awt.Font; import java.awt.Shape; import java.awt.BasicStroke; import java.awt.geom.*; import java.awt.AlphaComposite; import java.awt.image.ImageObserver; import java.net.URL; import javax.swing.ImageIcon; import javax.imageio.ImageIO; // import edu.tufts.vue.preferences.PreferencesManager; // import edu.tufts.vue.preferences.VuePrefEvent; // import edu.tufts.vue.preferences.implementations.ImageSizePreference; /** * Handle the presentation of an image resource, allowing resize. * Also provides special support for appear as a "node icon" -- a fixed * size image inside a node to represent it's resource. * * @version $Revision: 1.1 $ / $Date: 2009-10-27 15:03:56 $ / $Author: sfraize $ */ // TODO: on delete, null the image so it can be garbage collected, and on // un-delete, restore it via the resource, and hopefully it'll // still be in the memory cache (if not, it'll be in the disk cache) // TODO: update bad (error) images if preview gets good data // Better: handle this via listening to the resource for updates // (the LWCopmonent can do this), and if it's a CONTENT_CHANGED // update (v.s., say, a META_DATA_CHANGED), then we can refetch // the content. Actually, would still be nice if this happened // just by selecting the object, in case the resource previewer // didn't happen to be open. public class LWImage extends LWComponent // various impls could extend LWContainer or LWNode implements Images.Listener //,LWSelection.ControlListener //,edu.tufts.vue.preferences.VuePrefListener { private static final org.apache.log4j.Logger Log = org.apache.log4j.Logger.getLogger(LWImage.class); //private static final String ImageRepaint = LWKey.RepaintAsync; private static final String ImageRepaint = LWKey.Repaint; public static final boolean SLIDE_LABELS = false; //static int MaxRenderSize = PreferencesManager.getIntegerPrefValue(ImageSizePreference.getInstance()); //private static VueIntegerPreference PrefImageSize = ImageSizePreference.getInstance(); // is failing for some reason //static int MaxRenderSize = PrefImageSize.getValue(); public static final int DefaultMaxDimension = 128; private final static int MinWidth = 16; private final static int MinHeight = 16; enum Status { UNLOADED, LOADING, LOADED, ERROR, EMPTY }; // May want move most of this code to be done generically in URLResource, (e.g., // bytes size, byte progress & status are somewhat generic for all content), and // either just auto-handle the special case of width/height for everything, or add // generic properties based on content type for the URLResource (we sort of already // have this with stuff that comes from MapDropTarget). Tho we only need this for // content that has a needed/useful in-memory representation that's different than // it's on-disk content. Currently, this only applies to images. If we were to // support, say, dynamically generating icons (or even a document model) for HTML or // PDF content, the problem would then become a truly generic one. private static final double NO_ASPECT = -1; private Image mImage; private int mImageWidth = -1; // pixel width of raw image private int mImageHeight = -1; // pixel height of raw image private volatile Status mImageStatus = Status.UNLOADED; private double mImageAspect = NO_ASPECT; private long mDataSize = -1; private volatile long mDataSoFar = 0; private Object mUndoMarkForThread; //private double mImageScale = 1; // scale to the fixed size private double mRotation = 0; private Point2D.Float mOffset = new Point2D.Float(); // x & y always <= 0 /** is this image currently serving as an icon for an LWNode? */ private boolean isNodeIcon = false; // private transient LWIcon.Block mIconBlock = // new LWIcon.Block(this, // 20, 12, // null, // LWIcon.Block.VERTICAL); private void initImage() { disableProperty(LWKey.FontSize); // prevent 0 font size warnings (font not used on images) } public LWImage() { initImage(); setFillColor(null); //edu.tufts.vue.preferences.implementations.ImageSizePreference.getInstance().addVuePrefListener(this); } public LWImage(Resource r) { initImage(); if (r == null) throw new IllegalArgumentException("resource is not image content: " + r); if (!r.isImage()) Log.warn("making LWImage: may not be image content: " + r); setFillColor(null); setResource(r); //edu.tufts.vue.preferences.implementations.ImageSizePreference.getInstance().addVuePrefListener(this); } /** @return true -- an image is always it's own content */ @Override public boolean hasContent() { return true; } // // TODO: it isn't such a good idea to have every single LWImage ever created during the runtime // // be a listiner to the VUE preferences sub-system -- that means a pref event will be driving // // global, cross-map, cross-undo-queue events, which would be better managed from a single // // place in the model code. // public void preferenceChanged(VuePrefEvent prefEvent) // { // if (DEBUG.IMAGE) out("new pref value is " + ((Integer)ImageSizePreference.getInstance().getValue()).intValue()); // DefaultMaxDimension = ((Integer)prefEvent.getNewValue()).intValue(); // if (DEBUG.Enabled) System.out.println("New DefaultMaxDimension : " + DefaultMaxDimension + " in " + this); // if (mImage != null && isNodeIcon) // setMaxDimension(DefaultMaxDimension); // } @Override public LWImage duplicate(CopyContext cc) { // TODO: if had list of property keys in object, LWComponent // could handle all the duplicate code. LWImage i = (LWImage) super.duplicate(cc); i.mImage = mImage; i.mImageWidth = mImageWidth; i.mImageHeight = mImageHeight; i.mImageAspect = mImageAspect; i.isNodeIcon = isNodeIcon; i.mImageStatus = mImageStatus; i.setOffset(this.mOffset); i.setRotation(this.mRotation); return i; } // /** @return true */ // @Override // public boolean isImageNode() { // return true; // } @Override public boolean isAutoSized() { if (getClass().isAssignableFrom(LWNode.class)) return super.isAutoSized(); else return false; } /** @return false: images are never transparent */ @Override public boolean isTransparent() { if (false)// && this instanceof LWNode) return super.isTransparent(); else return false; } /** @return false: images are never translucent */ @Override public boolean isTranslucent() { if (false)// && this instanceof LWNode) { return super.isTranslucent(); else { // Technically, if there are any transparent pixels in the image, // we'd want to return true. return false; } } @Override public int getFocalMargin() { return 0; } public boolean isNodeIcon() { return isNodeIcon; } /** This currently makes LWImages invisible to selection (they're locked in their parent node */ //@Override protected LWComponent defaultPickImpl(PickContext pc) { if (getClass().isAssignableFrom(LWNode.class)) { return super.defaultPick(pc); } else { if (!hasFlag(Flag.SLIDE_STYLE) && isNodeIcon()) return pc.pickDepth > 0 ? this : getParent(); else return this; } } @Override public boolean supportsCopyOnDrag() { //return !hasFlag(Flag.SLIDE_STYLE); //return isNodeIcon(); return !hasFlag(Flag.SLIDE_STYLE) && isNodeIcon(); } /** @return true unless this is a node icon image */ @Override public boolean supportsUserResize() { //return !isNodeIcon(); return hasFlag(Flag.SLIDE_STYLE) || !isNodeIcon; } @Override public boolean supportsUserLabel() { return SLIDE_LABELS && hasFlag(Flag.SLIDE_STYLE); } // @Override // public Rectangle2D.Float getLayoutBounds() { // if (supportsUserLabel() && hasLabel()) { // final TextBox label = getLabelBox(); // final Rectangle2D.Float r = super.getLocalBounds(); // final float height = label.getBoxHeight() * 2; // r.y -= height; // r.height += height; // return r; // } else // return super.getLayoutBounds(); // } /** this for backward compat with old save files to establish the image as a special "node" image */ @Override public void XML_addNotify(Object context, String name, Object parent) { super.XML_addNotify(context, name, parent); if (parent instanceof LWNode) updateNodeIconStatus((LWNode)parent); } @Override protected void setParent(LWContainer parent) { super.setParent(parent); updateNodeIconStatus(parent); } /* protected void reparentNotify(LWContainer parent) { super.reparentNotify(parent); updateNodeIconStatus(parent); } */ private void updateNodeIconStatus(LWContainer parent) { //tufts.Util.printStackTrace("updateNodeIconStatus, mImage=" + mImage + " parent=" + parent); if (DEBUG.IMAGE) out("updateNodeIconStatus, mImage=" + Util.tags(mImage) + " parent=" + parent); if (parent == null) return; if (parent instanceof LWNode && parent.getChild(0) == this && getResource() != null && getResource().equals(parent.getResource())) { // special case: if first child of a LWNode is an LWImage, treat it as an icon isNodeIcon = true; if (mImageWidth <= 0) return; // if (!hasFlag(Flag.SLIDE_STYLE)) // setMaxDimension(DefaultMaxDimension); } else { isNodeIcon = false; if (super.width == NEEDS_DEFAULT) { // use icon size also as default size for plain (non-icon) images setMaxDimension(DefaultMaxDimension); } } } public void setMaxDimension(final double max) { if (DEBUG.IMAGE) out("setMaxDimension " + max); if (mImageWidth <= 0) { // this fixes the "gray link" which was being created when an image was // dropped into a node on a slide -- it's size was never being set, leaving // it infintesimally small / invisible, making it look like a link. (see // above condition on updateNodeIconStatus, where SLIDE_STYLE is checked). setSize((float)max, (float)max); return; } final double width = mImageWidth; final double height = mImageHeight; if (DEBUG.IMAGE) out("setMaxDimension curSize " + width + "x" + height); double newWidth, newHeight; if (width > height) { newWidth = max; newHeight = height * max / width; //newHeight = Math.round(height * max / width); } else { newHeight = max; newWidth = width * max / height; //newWidth = Math.round(width * max / height); } final float w = (float) newWidth; final float h = (float) newHeight; //if (DEBUG.IMAGE) out("setMaxDimension newSize " + newWidth + "x" + newHeight); if (DEBUG.IMAGE) out("setMaxDimension newSize " + w + "x" + h); setSize(w, h); } @Override protected TextBox getLabelBox() { if (super.labelBox == null) { initTextBoxLocation(super.getLabelBox()); //layoutImpl("LWImage.labelBox-init"); } return this.labelBox; } @Override public void initTextBoxLocation(TextBox textBox) { textBox.setBoxLocation(0, -textBox.getHeight()); } @Override public void layoutImpl(Object triggerKey) { if (false&&getClass().isAssignableFrom(LWNode.class)) { super.layoutImpl(triggerKey); } else { //mIconBlock.layout(); // if (super.labelBox != null) { // out("layoutImpl " + triggerKey + "; SET BOX LOCATION AT Y " + getHeight() + " in " + this); // super.labelBox.setBoxLocation(0, getHeight()); // } } } public Status getStatus() { return mImageStatus; } // TODO: this wants to be on LWComponent, in case this is a // regular node containing an LWImage, we want the image to // update, as it doesn't get selected. This depends on // how me might redo image support in maps tho, so // wait on that... @Override public void setSelected(boolean selected) { boolean wasSelected = isSelected(); super.setSelected(selected); if (selected && !wasSelected && mImageStatus == Status.ERROR && hasResource()) { //Util.printStackTrace("ADD SELCTED IMAGE CLEANUP " + this); // don't know if this really needs to be a cleanup task, // or just an after-AWT task, but safer to do one of them: // TODO: this may be conflicting with our new image update code, and this // would much better be handled in the ActiveComponentHandler than via a // cleanup task (which should generally be a solution of last resort) // See if we can handle this in VUE.checkForAndHandleResourceUpdate // Note that this code does however also deal with a missing // network resource suddenly appearing, and then we can // load the image from that addCleanupTask(new Runnable() { public void run() { if (VUE.getSelection().only() == LWImage.this) loadResourceImage(getResource(), null); }}); } } @Override public void setResource(Resource r) { setImageResource(r, false); } public void setNodeIconResource(Resource r) { setImageResource(r, true); } private void setImageResource(Resource r, boolean isNodeIconSync) { if (DEBUG.IMAGE) Log.debug("setImageResource " + r + "; isNodeIconSync=" + isNodeIconSync); if (r == null) { // this will happen normally if when the creation of a new image is undone // (altho this is kind of pointless: may want to just deny this, tho we // see zombie events if we do that) if (DEBUG.Enabled) out("nulling resource"); mImage = null; mImageWidth = -1; mImageHeight = -1; mImageStatus = Status.EMPTY; mImageAspect = NO_ASPECT; super.setResource(r); } else if (mXMLRestoreUnderway) { super.setResource(r); } else if (isNodeIcon() && !isNodeIconSync) { // we should be called back again with isNodeIconSync == true getParent().setResource(r); } else { mImage = null; mImageWidth = -1; mImageHeight = -1; mImageStatus = Status.UNLOADED; mImageAspect = NO_ASPECT; setResourceAndLoad(r, null); } } // todo: find a better way to do this than passing in an undo manager, which is dead ugly public void setResourceAndLoad(Resource r, UndoManager undoManager) { super.setResource(r); if (r != null) { setLabel(MapDropTarget.makeNodeTitle(r)); loadResourceImage(r, undoManager); } } public void reloadImage() { mImage = null; mImageStatus = Status.UNLOADED; notify(ImageRepaint); } /** @param um currently ignored (TODO) */ private void loadResourceImage(final Resource r, final UndoManager um) { //int width = r.getProperty("image.width", 64); //int height = r.getProperty("image.height", 64); final int suggestWidth = r.getProperty("image.width", -1); final int suggestHeight = r.getProperty("image.height", -1); // If we know a size before loading, this will get // us displaying that size. If not, we'll set // us to a minimum size for display until we // know the real size. if (suggestWidth > 0 && suggestHeight > 0 && (mImageWidth <= 0 || mImageHeight <= 0)) setImageSize(suggestWidth, suggestHeight); // if (mImageWidth <= 0 || mImageHeight <= 0) // setImageSize(width, height); // save a key that marks the current location in the undo-queue, // to be applied to the subsequent thread that make calls // to imageUpdate, so that all further property changes eminating // from that thread are applied to the same location in the undo queue. synchronized (this) { // If image is not immediately availble, need to mark current // place in undo key for changes that happen due to the image // arriving. We sync to be certian the key is set before // we can get any image callbacks. //Util.printStackTrace("GET IMAGE IN " + this); final boolean immediatelyAvailable = Images.getImage(r, this); if (immediatelyAvailable) mUndoMarkForThread = null; else mUndoMarkForThread = UndoManager.getKeyForNextMark(this); } } public boolean isCropped() { return mOffset.x < 0 || mOffset.y < 0; } /** @see Images.Listener */ public synchronized void gotImageSize(Object imageSrc, int width, int height, long byteSize) { if (DEBUG.IMAGE) out("gotImageSize " + width + "x" + height + " bytes=" + byteSize); mDataSize = byteSize; mImageStatus = Status.LOADING; if (mUndoMarkForThread == null) { if (DEBUG.IMAGE || DEBUG.UNDO || DEBUG.THREAD) out("gotImageSize: no undo key"); } // For the events triggered by the setSize below, make sure they go // to the right point in the undo queue. // The mark was generated synchronously in the main model accessing thread (AWT EDT), // so it should point to a sane place in the undo queue to add modifications as // a result of callbacks. if (!javax.swing.SwingUtilities.isEventDispatchThread()) UndoManager.attachCurrentThreadToMark(mUndoMarkForThread); // As this now calls autoShapeToAspect, it can trigger a setSize, // so it MUST be done after we've been attached to the proper // undo-mark in order for the undo to work properly. setImageSize(width, height); // If we're interrupted before this happens, and this is the drop of a new image, // we'll see a zombie event complaint from this setSize which is safely ignorable. // todo: suspend events if our thread was interrupted // don't set size if we are cropped: we're probably reloading from a saved .vue //if (isRawImage && isCropped() == false) { //if (isCropped() == false) { // if (super.width == NEEDS_DEFAULT) { // // if this is a new image object, set it's size to the image size (natural size) // setSize(width, height); // } updateNodeIconStatus(getParent()); layout(); notify(ImageRepaint); } private float mLastPct = 0; private int mLastPctEven = 0; private volatile String mStatusMsg; public void gotBytes(Object imageSrc, long bytesSoFar) { mDataSoFar = bytesSoFar; //out("BYTES SO FAR: " + bytesSoFar); // TODO: move this down to be recompute just before we actually draw if (mDataSize > 0 && mDataSoFar > 0) { // don't bother if we don't know the whole size yet... //final String statusMsg = Long.toString(mDataSoFar); final float pct = (float)mDataSoFar / (float)mDataSize; final int pctEven = Math.round(pct*100); //out("PCT: " + pct); if (pctEven > mLastPctEven) { // todo: if last update was more than, say 100ms ago (10fps) (statically: for ANY image), // also force an update mStatusMsg = Integer.toString(pctEven) + '%'; // todo: do in paint (no need do every time here) //out("notify on " + mStatusMsg); //mStatusMsg = String.format("%.1f%%", pct * 100); notify(ImageRepaint); } mLastPct = pct; mLastPctEven = pctEven; } } /** @see Images.Listener */ public synchronized void gotImage(Object imageSrc, Image image, int w, int h) { // Be sure to set the image before detaching from the thread, // or when the detach issues repaint events, we won't see the image. if (DEBUG.IMAGE) out("gotImage " + image); mImageStatus = Status.LOADED; setImageSize(w, h); //mImageWidth = w; //mImageHeight = h; mImage = image; mLastPct = mLastPctEven = 0; mStatusMsg = "(Load)"; //if (isRawImage && isCropped() == false) //if (isCropped() == false) // setSize(w, h); updateNodeIconStatus(getParent()); if (mUndoMarkForThread == null) { //notify(LWKey.RepaintAsync); } else { // in case this thread get's re-used: UndoManager.detachCurrentThread(mUndoMarkForThread); mUndoMarkForThread = null; } notify(ImageRepaint); // Any problem using the Image Fetcher thread to do this? //if (getResource() instanceof MapResource) //((MapResource)getResource()).scanForMetaData(LWImage.this, true); } /** @see Images.Listener */ public synchronized void gotImageError(Object imageSrc, String msg) { // set image dimensions so if we resize w/out image it works mImageStatus = Status.ERROR; mImageWidth = (int) getWidth(); mImageHeight = (int) getHeight(); if (mImageWidth < 1) { mImageWidth = 128; mImageHeight = 128; setSize(128,128); } notify(ImageRepaint); } @Override public void setToNaturalSize() { setSize(mImageWidth, mImageHeight); } // public void X_setSize(float w, float h) { // super.setSize(w, h); // // Even if we don't have an image yet, we need to keep these set in case user attemps to resize the frame. // // They can still crop down if they like, but this prevents them from making it any bigger. // if (mImageWidth < 0) // mImageWidth = (int) getWidth(); // if (mImageHeight < 0) // mImageHeight = (int) getHeight(); // } /** record the actual pixel dimensions of the underlying raw image */ void setImageSize(int w, int h) { mImageWidth = w; mImageHeight = h; mImageAspect = ((double)w) / ((double)h); // todo: may want to just always update the node status here -- covers most cases, plus better when the drop code calls this? if (DEBUG.IMAGE) out("setImageSize " + w + "x" + h + " aspect=" + mImageAspect); // SMF 2008-05-06: can't make sense of this: if we replace the image content, and the // old content aspect != 1, and the new content aspect == 1, this prevents us from // updating our shape to the new 1.0 aspect (and squeezes/stretches the 1.0 aspect image // into the old aspect shape) // // If below stops autoShapeToAspect from being called with default data, // // as well as cases where it'd be moot anyway. // if (!(w == h && mImageAspect == 1.0)) autoShapeToAspect(); //setAspect(aspect); // LWComponent too paternal for us right now } @Override public void setSize(float w, float h) { if (DEBUG.IMAGE||DEBUG.WORK) out("setSize " + w + "x" + h); super.setSize(w, h); } private void autoShapeToAspect() { if (mImageAspect > 0) { if (this.width == NEEDS_DEFAULT || this.height == NEEDS_DEFAULT) { //Log.error("cannot auto-shape without request size: " + this, new Throwable("HERE")); if (DEBUG.WORK||DEBUG.IMAGE) out("autoshaping from scratch to " + DefaultMaxDimension); setMaxDimension(DefaultMaxDimension); return; } //if (DEBUG.IMAGE) out("autoShapeToAspect in: " + width + "," + height); final Size newSize = ConstrainToAspect(mImageAspect, this.width, this.height); final float dw = this.width - newSize.width; final float dh = this.height - newSize.height; /* * Added this in response to VUE-948 */ if ((DEBUG.WORK || DEBUG.IMAGE) && (newSize.width != width || newSize.height != height)) out(String.format("autoShapeToAspect: a=%.2f dw=%g dh=%g; %.1fx%.1f -> %s", mImageAspect, dw, dh, width, height, newSize)); //out("autoShapeToAspect: a=" + mImageAspect + "; dw=" + dw + ", dh=" + dh + "; " + width + "," + height + " -> adj " + newSize); //out("autoShapeToAspect: " + width + "," + height + " -> newSize: " + newSize.width + "," + newSize.height); if (Math.abs(dw) > 1 || Math.abs(dh) > 1) { // above check helps reduce needless tweaks, which make things messy during map loading setSize(newSize.width, newSize.height); } } } /** * Don't let us get bigger than the size of our image, or * smaller than MinWidth/MinHeight. */ protected void userSetSize(float width, float height, MapMouseEvent e) { if (DEBUG.IMAGE) out("userSetSize " + Util.fmt(new Point2D.Float(width, height)) + "; e=" + e); if (e != null && e.isShiftDown()) { // Unconstrained aspect ration scaling super.userSetSize(width, height, e); } else if (mImageAspect > 0) { Size newSize = ConstrainToAspect(mImageAspect, width, height); setSize(newSize.width, newSize.height); } else setSize(width, height); // If (e != null && e.isShiftDown()) // croppingSetSize(width, height); // else // scalingSetSize(width, height); } private void scalingSetSize(float width, float height) { /* if (DEBUG.IMAGE) out("scalingSetSize0 " + width + "x" + height); if (mImageWidth + mOffset.x < width) width = mImageWidth + mOffset.x; if (mImageHeight + mOffset.y < height) height = mImageHeight + mOffset.y; if (width < MinWidth) width = MinWidth; if (height < MinHeight) height = MinHeight; */ if (DEBUG.IMAGE) out("scalingSetSize1 " + width + "x" + height); setSize(width, height); } /** this leaves the image exactly as it is, and just resizes the cropping region */ private void croppingSetSize(float width, float height) { //if (DEBUG.IMAGE) out("croppingSetSize0 " + width + "x" + height); if (mImageWidth + mOffset.x < width) width = mImageWidth + mOffset.x; if (mImageHeight + mOffset.y < height) height = mImageHeight + mOffset.y; if (width < MinWidth) width = MinWidth; if (height < MinHeight) height = MinHeight; if (DEBUG.IMAGE) out("croppingSetSize1 " + width + "x" + height); final float oldAspect = super.mAspect; super.mAspect = 0; // don't pay attention to aspect when cropping super.setSize(width, height); super.mAspect = oldAspect; } /* @param r - requested LWImage frame in map coordinates */ //private void constrainFrameToImage(Rectangle2D.Float r) {} /** * When user changes a frame on the image, if the location changes, * attempt to keep our content image in the same place (e.g., make * it look like we're just moving a the clip-region, if the LWImage * is smaller than the size of the underlying image). */ public void X_RESIZE_CONTROL_HACK_userSetFrame(float x, float y, float w, float h, MapMouseEvent e) { if (DEBUG.IMAGE) out("userSetFrame0 " + VueUtil.out(new Rectangle2D.Float(x, y, w, h))); if (w < MinWidth) { if (x > getX()) // dragging left edge right: hold it back x -= MinWidth - w; w = MinWidth; } if (h < MinHeight) { if (y > getY()) // dragging top edge down: hold it back y -= MinHeight - h; h = MinHeight; } Point2D.Float off = new Point2D.Float(mOffset.x, mOffset.y); off.x += getX() - x; off.y += getY() - y; //if (DEBUG.IMAGE) out("tmpoff " + VueUtil.out(off)); if (off.x > 0) { x += off.x; w -= off.x; off.x = 0; } if (off.y > 0) { y += off.y; h -= off.y; off.y = 0; } setOffset(off); if (DEBUG.IMAGE) out("userSetFrame1 " + VueUtil.out(new Rectangle2D.Float(x, y, w, h))); userSetSize(w, h, e); setLocation(x, y); } public static final Key KEY_Rotation = new Key("image.rotation", KeyType.STYLE) { // rotation in radians public void setValue(LWComponent c, Object val) { ((LWImage)c).setRotation(((Double)val).doubleValue()); } public Object getValue(LWComponent c) { return new Double(((LWImage)c).getRotation()); } }; public void setRotation(double rad) { Object old = new Double(mRotation); this.mRotation = rad; notify(KEY_Rotation, old); } public double getRotation() { return mRotation; } public static final Key Key_ImageOffset = new Key("image.pan", KeyType.STYLE) { public void setValue(LWComponent c, Object val) { ((LWImage)c).setOffset((Point2D)val); } public Object getValue(LWComponent c) { return ((LWImage)c).getOffset(); } }; public void setOffset(Point2D p) { if (p.getX() == mOffset.x && p.getY() == mOffset.y) return; Object oldValue = new Point2D.Float(mOffset.x, mOffset.y); if (DEBUG.IMAGE) out("LWImage setOffset " + VueUtil.out(p)); this.mOffset.setLocation(p.getX(), p.getY()); notify(Key_ImageOffset, oldValue); } public Point2D getOffset() { return new Point2D.Float(mOffset.x, mOffset.y); } public int getImageWidth() { return mImageWidth; } public int getImageHeight() { return mImageHeight; } /* static LWImage testImage() { LWImage i = new LWImage(); i.imageIcon = VueResources.getImageIcon("vueIcon32x32"); i.setSize(i.mImageWidth, i.mImageHeight); return i; } */ private Shape getClipShape() { //return super.drawnShape; // todo: cache & handle knowing if we need to update return new Rectangle2D.Float(0,0, getWidth(), getHeight()); } private static final AlphaComposite HudTransparency = AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 0.1f); //private static final Color IconBorderColor = new Color(255,255,255,64); //private static final Color IconBorderColor = new Color(0,0,0,64); // screwing up in composite drawing //private static final Color IconBorderColor = Color.gray; // FOR LWNode IMPL: /* protected void drawNode(DrawContext dc) { // do this for implmemented as a subclass of node drawImage(dc); super.drawNode(dc); } */ // public void drawRaw(DrawContext dc) { // // (skip default composite cleanup for images) // drawImpl(dc); // } // REMOVE FOR LWNode IMPL: @Override protected void drawImpl(DrawContext dc) { LWComponent grandparent; // if (!isSelected() && (parent == null || (!parent.isSelected() && ((grandparent = parent.parent) == null || !grandparent.isSelected())))) { // double alpha = VUE.getInteractionToolsPanel().getAlpha(); // if (alpha != 1) { // // "Fade" this image. // dc.setAlpha(alpha); // } // } drawWithoutShape(dc); // if (dc.g.getComposite() instanceof AlphaComposite) { // AlphaComposite a = (AlphaComposite) dc.g.getComposite(); // System.err.println("ALPHA RULE: " + a.getRule() + " " + DrawContext.AlphaRuleNames[a.getRule()] + " " + this); // } // if (dc.focal == this) // super.drawChildren(dc); } private boolean isIndicatedIn(DrawContext dc) { if (dc.hasIndicated()) { final LWComponent indication = dc.getIndicated(); return indication == this || (isNodeIcon() && getParent() == indication); } else { return false; } } public void drawWithoutShape(DrawContext dc) { // see comments on VUE-892 - this code does not seem to present the right behavior for search // please re-enable this code and reopen the bug if this code is needed- Dan H /* if (isNodeIcon()) { final LWComponent parent = getParent(); if (parent != null && parent.isFiltered()) { // this is a hack because images are currently special cased as tied to their parent node return; } }*/ final Shape shape = getZeroShape(); if (isSelected() && dc.isInteractive() && dc.focal != this) { dc.g.setColor(COLOR_HIGHLIGHT); dc.g.setStroke(new BasicStroke(getStrokeWidth() + SelectionStrokeWidth)); dc.g.draw(shape); } final boolean indicated = isIndicatedIn(dc); if (indicated && dc.focal != this) { Color c = getParent().getRenderFillColor(dc); if (VueUtil.isTranslucent(c)) c = Color.gray; dc.g.setColor(c); dc.g.fill(shape); } if (isNodeIcon && dc.focal != this) { if (!indicated && DefaultMaxDimension > 0) drawImageBox(dc); // Forced border for node-icon's: if ((mImage != null || indicated) && !getParent().isTransparent() && DefaultMaxDimension > 0) { // this is somehow making itext PDF generation through a GC worse... (probably just a bad tickle) dc.g.setStroke(STROKE_TWO); //dc.g.setColor(IconBorderColor); dc.g.setColor(getParent().getRenderFillColor(dc).darker()); dc.g.draw(shape); } } else if (!indicated) { if (!super.isTransparent()) { final Color fill = getFillColor(); if (fill == null || fill.getAlpha() == 0) { Util.printStackTrace("isTransparent lied about fill " + fill); } else { dc.g.setColor(fill); dc.g.fill(shape); } } drawImageBox(dc); if (getStrokeWidth() > 0) { dc.g.setStroke(this.stroke); dc.g.setColor(getStrokeColor()); dc.g.draw(shape); } if (supportsUserLabel() && hasLabel()) { initTextBoxLocation(getLabelBox()); if (this.labelBox.getParent() == null) { dc.g.translate(labelBox.getBoxX(), labelBox.getBoxY()); this.labelBox.draw(dc); } } } //super.drawImpl(dc); // need this for label } /** For interactive images as separate objects, which are currently disabled */ /* private void drawInteractive(DrawContext dc) { drawPathwayDecorations(dc); drawSelectionDecorations(dc); dc.g.translate(getX(), getY()); float _scale = getScale(); if (_scale != 1f) dc.g.scale(_scale, _scale); // if (getStrokeWidth() > 0) { // dc.g.setStroke(new BasicStroke(getStrokeWidth() * 2)); // dc.g.setColor(getStrokeColor()); // dc.g.draw(new Rectangle2D.Float(0,0, getWidth(), getHeight())); // } drawImage(dc); if (getStrokeWidth() > 0) { dc.g.setStroke(this.stroke); dc.g.setColor(getStrokeColor()); dc.g.draw(new Rectangle2D.Float(0,0, getWidth(), getHeight())); } if (isSelected() && dc.isInteractive()) { dc.g.setComposite(HudTransparency); dc.g.setColor(Color.WHITE); dc.g.fill(mIconBlock); dc.g.setComposite(AlphaComposite.Src); // TODO: set a clip so won't draw outside // image bounds if is very small mIconBlock.draw(dc); } if (_scale != 1f) dc.g.scale(1/_scale, 1/_scale); dc.g.translate(-getX(), -getY()); } */ private void drawImageBox(DrawContext dc) { if (mImage == null && !dc.isIndicated(this)) drawImageStatus(dc); else drawImage(dc); if (mImageStatus == Status.UNLOADED && getResource() != null) { // Doing this here (in a draw method) prevents images from loading until // they actually attempt to paint, which is handy when loading a map with // lots of large images: you can quickly see the map before the images need // to start loading. Also handy if loading a large number of maps at once // -- images on undisplayed maps won't start to load until the first time // they're asked to paint. synchronized (this) { if (mImageStatus == Status.UNLOADED) { mImageStatus = Status.LOADING; // TODO: running this on AWT can cause problems during map loading, // as events on an image load thread we want to be ignoring in terms // of map modifications will be taken seriously when appearing on // the AWT thread. We need a better system for ignoring image // events during map loading (events that don't want to be incrementing // the map modification count). We should at least include new code // to ignore all events prior to the first user event, which will at // least catch everything that happens before the very first undo mark. if (DEBUG.IMAGE) out("invokeLater loadResourceImage " + getResource()); tufts.vue.gui.GUI.invokeAfterAWT(new Runnable() { public void run() { loadResourceImage(getResource(), null); }}); } } } } @Override public void XML_completed(Object context) { super.XML_completed(context); if (super.width < MinWidth || super.height < MinHeight) { Log.info(String.format("bad size: adjusting to minimum %dx%d: %s", MinWidth, MinHeight, this)); super.width = MinWidth; super.height = MinHeight; } // // This will cause images to start loading during parsing of persisted map files: // if (mImageStatus == Status.UNLOADED) { // mImageStatus = Status.LOADING; // loadResourceImage(getResource(), null); // } } private void drawImage(DrawContext dc) { final AffineTransform transform = new AffineTransform(); // private static final AlphaComposite MatteTransparency = AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 0.5f); // Todo: when/if put this back in, see if we can handle it in the ImageTool so we don't need active tool in the DrawContext // if (isSelected() && dc.isInteractive() && dc.getActiveTool() instanceof ImageTool) { // dc.g.setComposite(MatteTransparency); // dc.g.drawImage(mImage, transform, null); // dc.g.setComposite(AlphaComposite.Src); // } if (false && isCropped()) { Shape oldClip = dc.g.getClip(); dc.g.clip(getClipShape()); // this clip/restore clip may be screwing up our PDF export library dc.g.drawImage(mImage, transform, null); dc.g.setClip(oldClip); } else { //dc.g.clip(super.drawnShape); transform.scale(getWidth() / mImageWidth, getHeight() / mImageHeight); dc.g.drawImage(mImage, transform, null); //dc.g.drawImage(mImage, 0, 0, (int)super.width, (int)super.height, null); //dc.g.drawImage(mImage, 0, 0, mImageWidth, mImageHeight, null); } } private static final Color EmptyColorDark = new Color(0,0,0,128); private static final Color LoadedColorDark = new Color(0,0,0,160); private static final Color EmptyColorLight = new Color(128,128,128,64); private static final Color LoadedColorLight = new Color(128,128,128,128); private Color getEmptyColor(DrawContext dc) { Color fill = getFinalFillColor(dc); return Color.black.equals(fill) ? EmptyColorLight : EmptyColorDark; // final LWComponent parent = getParent(); // Color pc; // if (parent != null && (pc=parent.getRenderFillColor(dc)) != null) { // return Color.black.equals(pc) ? EmptyColorLight : EmptyColorDark; // } else if (dc.getBackgroundFill() != null && dc.getBackgroundFill().equals(Color.black)) // return EmptyColorLight; // else // return EmptyColorDark; } private Color getLoadedColor(DrawContext dc) { final LWComponent parent = getParent(); Color pc; if (parent != null && (pc=parent.getRenderFillColor(dc)) != null) { return Color.black.equals(pc) ? LoadedColorLight : LoadedColorDark; } else return LoadedColorDark; } private static final int StatusHeight = 4; private static final Font StatusFont = new Font("Gill Sans", Font.PLAIN, 10); //private static final String LoadingText = "Loading..."; //private static final float LoadingWidth = (float) tufts.vue.gui.GUI.stringWidth(StatusFont, LoadingText); private void drawImageStatus(DrawContext dc) { String status1 = "Loading..."; String status2 = null; float pct = 0; synchronized (this) { if (mImageStatus == Status.ERROR) { status1 = "Missing"; status2 = "Image"; } else if (mImageStatus == Status.EMPTY) { status1 = "Empty Image"; status2 = "(no resource)"; } else if (mDataSoFar > 0 && mStatusMsg != null) { status1 = mStatusMsg; pct = mLastPct; } } // if (mDataSoFar > 0 && mStatusMsg != null) { // // //final String statusMsg = Long.toString(mDataSoFar); // // final float pct = (float)mDataSoFar / (float)mDataSize; // // //out("PCT: " + pct); // // final String statusMsg = String.format("%.1f%%", pct * 100); // final float statusWidth = (float) tufts.vue.gui.GUI.stringWidth(StatusFont, mStatusMsg); // dc.g.drawString(mStatusMsg, (width-statusWidth)/2, (height+StatusHeight)/2); // //dc.g.drawString(""+mDataSize, 0, height-20); // //dc.g.drawString(""+mDataSoFar, 0, height); // } else { // dc.g.drawString("Loading...", (width-LoadingWidth)/2, (height+StatusHeight)/2); // } final int width = (int) getWidth(); final int height = (int) getHeight(); if (pct > 0) { final int split = (int) (width * pct); dc.g.setColor(getLoadedColor(dc)); dc.g.fillRect(0, 0, split, height); dc.g.setColor(getEmptyColor(dc)); dc.g.fillRect(split, 0, width - split, height); } else { dc.g.setColor(getEmptyColor(dc)); dc.g.fillRect(0, 0, width, height); } dc.g.setColor(Color.lightGray); dc.g.setFont(StatusFont); // todo: scale to on-screen image size (e.g., can't see with big raw images) if (status2 != null) { drawStatusLine(dc, status1, -5); drawStatusLine(dc, status2, +5); } else drawStatusLine(dc, status1, 0); } private void drawStatusLine(DrawContext dc, String text, int yoff) { final float textWidth = (float) tufts.vue.gui.GUI.stringWidth(StatusFont, text); dc.g.drawString(text, (getWidth()-textWidth)/2, (getHeight()+StatusHeight)/2 + yoff); } /* protected void drawImage(DrawContext dc) { if (mImage == null) { int w = (int) getWidth(); int h = (int) getHeight(); if (mImageError) dc.g.setColor(ErrorColor); else dc.g.setColor(Color.darkGray); dc.g.fillRect(0, 0, w, h); dc.g.setColor(Color.lightGray); dc.g.drawRect(0, 0, w, h); // can't see this line at small scales return; } AffineTransform transform = AffineTransform.getTranslateInstance(mOffset.x, mOffset.y); if (mRotation != 0 && mRotation != 360) transform.rotate(mRotation, getImageWidth() / 2, getImageHeight() / 2); if (isSelected() && dc.isInteractive() && dc.getActiveTool() instanceof ImageTool) { dc.g.setComposite(MatteTransparency); dc.g.drawImage(mImage, transform, null); dc.g.setComposite(AlphaComposite.Src); } if (isRawImage) { Shape oldClip = dc.g.getClip(); dc.g.clip(getClipShape()); dc.g.drawImage(mImage, transform, null); dc.g.setClip(oldClip); } else { dc.g.drawImage(mImage, 0, 0, mImageWidth, mImageHeight, null); } } */ // @Override // public void mouseOver(MapMouseEvent e) // { // if (getClass().isAssignableFrom(LWNode.class)) // super.mouseOver(e); // else // mIconBlock.checkAndHandleMouseOver(e); // } // Holy shit: if we somehow defined all this control-point stuff as a property editor, // could we then just attach the property editor to any component that // supported that property? E.g. -- could help enormously with having // a merged LWNode and LWImage. Not sure we REALLY want this tho. // Still need to figure out what to do with shape on the LWImage.... private transient Point2D.Float dragStart; private transient Point2D.Float offsetStart; private transient Point2D.Float imageStart; // absolute map location of 0,0 in the image private transient Point2D.Float locationStart; /** interface ControlListener handler */ public void controlPointPressed(int index, MapMouseEvent e) { //out("control point " + index + " pressed"); offsetStart = new Point2D.Float(mOffset.x, mOffset.y); locationStart = new Point2D.Float(getX(), getY()); dragStart = e.getMapPoint(); imageStart = new Point2D.Float(getX() + mOffset.x, getY() + mOffset.y); } /** interface ControlListener handler */ public void controlPointMoved(int index, MapMouseEvent e) { if (index == 0) { if (mImageStatus == Status.ERROR) // don't let user play with offset if no image visible return; float deltaX = dragStart.x - e.getMapX(); float deltaY = dragStart.y - e.getMapY(); if (e.isShiftDown()) { dragCropImage(deltaX, deltaY); } else { dragMoveCropRegion(deltaX, deltaY); } } else throw new IllegalArgumentException(this + " no such control point"); } private void dragCropImage(float deltaX, float deltaY) { Point2D.Float off = new Point2D.Float(); // drag frame around on underlying image // we need to constantly adjust offset to keep // it fixed in absolute map coordinates. Point2D.Float loc = new Point2D.Float(); loc.x = locationStart.x - deltaX; loc.y = locationStart.y - deltaY; off.x = offsetStart.x + deltaX; off.y = offsetStart.y + deltaY; constrainLocationToImage(loc, off); setOffset(off); setLocation(loc); } private void dragMoveCropRegion(float deltaX, float deltaY) { Point2D.Float off = new Point2D.Float(); // drag underlying image around within frame off.x = offsetStart.x - deltaX; off.y = offsetStart.y - deltaY; constrainOffset(off); setOffset(off); } /** Keep LWImage filled with image bits (never display "area" outside of the image) */ private void constrainOffset(Point2D.Float off) { if (off.x > 0) off.x = 0; if (off.y > 0) off.y = 0; if (off.x + getImageWidth() < getWidth()) off.x = getWidth() - getImageWidth(); if (off.y + getImageHeight() < getHeight()) off.y = getHeight() - getImageHeight(); } /** Keep LWImage filled with image bits (never display "area" outside of the image) * Used for constraining the clipped region to the underlying image, which we keep * fixed at an absolute map location in this constraint. */ private void constrainLocationToImage(Point2D.Float loc, Point2D.Float off) { if (off.x > 0) { loc.x += mOffset.x; off.x = 0; } if (off.y > 0) { loc.y += mOffset.y; off.y = 0; } // absolute image image location should never change from imageStart // Keep us from panning beyond top or left Point2D.Float image = new Point2D.Float(loc.x + off.x, loc.y + off.y); if (image.x < imageStart.x) { //System.out.println("home left"); loc.x = imageStart.x; off.x = 0; } if (image.y < imageStart.y) { //System.out.println("home top"); loc.y = imageStart.y; off.y = 0; } // Keep us from panning beyond right or bottom if (getImageWidth() + off.x < getWidth()) { //System.out.println("out right"); loc.x = (imageStart.x + getImageWidth()) - getWidth(); off.x = getWidth() - getImageWidth(); } if (getImageHeight() + off.y < getHeight()) { //System.out.println("out bot"); loc.y = (imageStart.y + getImageHeight()) - getHeight(); off.y = getHeight() - getImageHeight(); } } /** interface ControlListener handler */ public void controlPointDropped(int index, MapMouseEvent e) { if (DEBUG.IMAGE) out("control point " + index + " dropped"); } // private LWSelection.Controller[] controlPoints = new LWSelection.Controller[1]; // /** interface ControlListener */ // public LWSelection.Controller[] X_getControlPoints() // DEIMPLEMENTED // { // controlPoints[0] = new LWSelection.Controller(getMapCenterX(), getMapCenterY()); // controlPoints[0].setColor(null); // no fill (transparent) // return controlPoints; // } @Override public String paramString() { return super.paramString() + " " + mImageStatus + " raw=" + mImageWidth + "x" + mImageHeight + (isNodeIcon ? " <NodeIcon>" : ""); } /* private void loadImageAsync(MapResource r) { Object content = new Object(); try { content = r.getContent(); imageIcon = (ImageIcon) content; } catch (ClassCastException cce) { cce.printStackTrace(); System.err.println("getContent didn't return ImageIcon: got " + content.getClass().getName() + " from " + r.getClass() + " " + r); imageIcon = null; //if (DEBUG.CASTOR) System.exit(0); } catch (Exception e) { e.printStackTrace(); System.err.println("error getting " + r); } // don't set size if this is during a restore [why not?], which is the only // time width & height should be allowed less than 10 // [ What?? ] -- todo: this doesn't work if we're here because the resource was changed... //if (this.width < 10 && this.height < 10) if (imageIcon != null) { int w = imageIcon.getIconWidth(); int h = imageIcon.getIconHeight(); if (w > 0 && h > 0) setSize(w, h); } layout(); notify(LWKey.RepaintComponent); } */ // TODO: have the LWMap make a call at the end of a restore to all LWComponents // telling them to start loading any media they need. Pass in a media tracker // that the LWMap and/or MapViewer can use to track/report the status of // loading, and know when it's 100% complete. // Note: all this code will likely be superceeded by generic content // loading & caching code, in which case we may not be using // an ImageObserver anymore, just a generic input stream, tho actually, // we wouldn't have the chance to get the size as soon as it comes in, // so probably not all will be superceeded. // TODO: problem: if you drop a second image before the first one // has finished loading, both will try and set an undo mark for their thread, // but the're both in the Image Fetcher thread! So we're going to need // todo our own loading after all, as I see no way for the UndoManager // to tell between events coming in on the same thread, unless maybe // the mark can be associated with a particular object? I guess that // COULD work: all the updates are just happening on the LWImage... // Well, not exactly: the parent could resize due to setting the image // size, tho that would be overriden by the un-drop of the image // and removing it as child -- oh, but the hierarchy event wouldn't get // tagged, so it would have be tied to any events that TOUCH that object, // which does not work anyway as the image could be user changed. Well, // no, that would be detected by it coming from the unmarked thread. // So any event coming from the thread and "touching" this object could // be done, but that's just damn hairy... // Well, UndoManager is coalescing them for now, which seems to // work pretty well, but will probably break if user drops more // than one image and starts tweaking anyone but the first one before they load /* private void XloadImage(MapResource mr, UndoManager undoManager) { if (DEBUG.IMAGE || DEBUG.THREAD) out("loadImage"); Image image = XgetImage(mr); // this will immediately block if host not responding // todo: okay, we can skip the rest of this code as getImage now uses the ImageIO // fetch if (image == null) { mImageError = true; return; } // don't bother to set mImage here: JVM's no longer do drawing of available bits if (DEBUG.IMAGE) out("prepareImage on " + image); if (mUndoMarkForThread != null) { Util.printStackTrace("already have undo key " + mUndoMarkForThread); mUndoMarkForThread = null; } if (java.awt.Toolkit.getDefaultToolkit().prepareImage(image, -1, -1, this)) { if (DEBUG.IMAGE || DEBUG.THREAD) out("ALREADY LOADED"); mImage = image; setRawImageSize(image.getWidth(null), image.getHeight(null)); // If the size hasn't already been set, set it. //if (getWidth() < 10 && getHeight() < 10) setSize(mImageWidth, mImageHeight); notify(LWKey.RepaintAsync); } else { if (DEBUG.IMAGE || DEBUG.THREAD) out("ImageObserver Thread kicked off"); mDebugChar = sDebugChar; if (++sDebugChar > 'Z') sDebugChar = 'A'; // save a key that marks the current location in the undo-queue, // to be applied to the subsequent thread that make calls // to imageUpdate, so that all further property changes eminating // from that thread are applied to the same location in the undo queue. if (undoManager == null) mUndoMarkForThread = UndoManager.getKeyForNextMark(this); else mUndoMarkForThread = undoManager.getKeyForNextMark(); } } private Image XgetImage(MapResource mr) { URL url = mr.asURL(); if (url == null) return null; Image image = null; try { // This allows reading of .tif & .bmp in addition to standard formats. // We'll eventually want to use this for everything, and cache // Resource objects themselves, but ImageIO caching doesn't // appear to be working right now, so we only use it if we have to. // .ico comes from a 3rd party library: aclibico.jar String s = mr.getSpec().toLowerCase(); if (s.endsWith(".tif") || s.endsWith(".tiff") || s.endsWith(".bmp") || s.endsWith(".ico")) image = ImageIO.read(url); } catch (Throwable t) { if (DEBUG.Enabled) Util.printStackTrace(t); VUE.Log.info(url + ": " + t); } if (image != null) return image; // If the host isn't responding, Toolkit.getImage will block for a while. It // will apparently ALWAYS eventually get an Image object, but if it failed, we // eventually get callback to imageUpdate (once prepareImage is called) with an // error code. In any case, if you don't want to block, this has to be done in // a thread. String s = mr.getSpec(); if (s.startsWith("file://")) { // TODO: SEE Util.java: WINDOWS URL'S DON'T WORK IF START WITH FILE:// // (two slashes), MUST HAVE THREE! move this code to MapResource; find // out if can even force a URL to have an extra slash in it! Report // this as a java bug. // TODO: Our Cup>>Chevron unicode char example is failing // here on Windows (tho it works for windows openURL). // (The image load fails) // Try ensuring the URL is UTF-8 first. s = s.substring(7); if (DEBUG.IMAGE || DEBUG.THREAD) out("getImage " + s); image = java.awt.Toolkit.getDefaultToolkit().getImage(s); } else { if (DEBUG.IMAGE || DEBUG.THREAD) out("getImage"); image = java.awt.Toolkit.getDefaultToolkit().getImage(url); } if (image == null) Util.printStackTrace("image is null"); return image; } */ /* private static char sDebugChar = 'A'; private char mDebugChar; public boolean XimageUpdate(Image img, int flags, int x, int y, int width, int height) { if ((DEBUG.IMAGE||DEBUG.THREAD) && (DEBUG.META || (flags & ImageObserver.SOMEBITS) == 0)) { if ((flags & ImageObserver.ALLBITS) != 0) System.err.println(""); out("imageUpdate; flags=(" + flags + ") " + width + "x" + height); } if ((flags & ImageObserver.ERROR) != 0) { if (DEBUG.IMAGE) out("ERROR"); mImageError = true; // set image dimensions so if we resize w/out image it works mImageWidth = (int) getWidth(); mImageHeight = (int) getHeight(); if (mImageWidth < 1) { mImageWidth = 100; mImageHeight = 100; setSize(100,100); } notify(LWKey.RepaintAsync); return false; } if (DEBUG.IMAGE || DEBUG.THREAD) { if ((flags & ImageObserver.SOMEBITS) == 0) { //out("imageUpdate; flags=(" + flags + ") "); //+ thread + " 0x" + Integer.toHexString(thread.hashCode()) //+ " " + sun.awt.AppContext.getAppContext() //Thread thread = Thread.currentThread(); //System.out.println("\n" + getResource() + " (" + flags + ") " //+ thread + " 0x" + Integer.toHexString(thread.hashCode()) //+ " " + sun.awt.AppContext.getAppContext()); } else { // Print out a letter indicating the next batch of bits has come in System.err.print(mDebugChar); } } if ((flags & ImageObserver.WIDTH) != 0 && (flags & ImageObserver.HEIGHT) != 0) { //XsetRawImageSize(width, height); if (DEBUG.IMAGE || DEBUG.THREAD) out("imageUpdate; got size " + width + "x" + height); if (mUndoMarkForThread == null) { if (DEBUG.Enabled) out("imageUpdate: no undo key"); } // For the events triggered by the setSize below, make sure they go // to the right point in the undo queue. UndoManager.attachCurrentThreadToMark(mUndoMarkForThread); // If we're interrupted before this happens, and this is the drop of a new image, // we'll see a zombie event complaint from this setSize which is safely ignorable. // todo: suspend events if our thread was interrupted if (isCropped() == false) { // don't set size if we are cropped: we're probably reloading from a saved .vue setSize(width, height); } layout(); notify(LWKey.RepaintAsync); } if (false) { // the drawing of partial image results not working in current MacOSX JVM's! mImage = img; System.err.print("+"); notify(LWKey.RepaintAsync); } if ((flags & ImageObserver.ALLBITS) != 0) { imageLoadSucceeded(img); return false; } // We're sill getting data: return true. // Unless we've been interrupted: should abort and return false. if (Thread.interrupted()) { if (DEBUG.Enabled || DEBUG.IMAGE || DEBUG.THREAD) System.err.println("\n" + getResource() + " *** INTERRUPTED *** " + Thread.currentThread()); //System.err.println("\n" + getResource() + " *** INTERRUPTED *** (lowering priority) " + thread); // Changing priority of the Image Fetcher will prob slow down all subsequent loads //thread.setPriority(Thread.MIN_PRIORITY); // let it finish anyway for now, as we don't yet handle restarting this // operation if they Redo return true; // This is also not good enough: we're going to need to get an undo // key right at the start as we might get interrupted even // before the getImage returns.. //return false; } else return true; } private void imageLoadSucceeded(Image image) { // Be sure to set the image before detaching from the thread, // or when the detach issues repaint events, we won't see the image. mImage = image; if (mUndoMarkForThread == null) { notify(LWKey.RepaintAsync); } else { UndoManager.detachCurrentThread(mUndoMarkForThread); // in case our ImageFetcher get's re-used // todo: oh, crap, what if this image fetch thread is attached // to another active image load? mUndoMarkForThread = null; } if (DEBUG.Enabled) { String[] tryProps = new String[] { "name", "title", "description", "comment" }; for (int i = 0; i < tryProps.length; i++) { Object p = image.getProperty(tryProps[i], null); if (p != null && p != java.awt.Image.UndefinedProperty) System.err.println("FOUND PROPERTY " + tryProps[i] + "=" + p); } } // Any problem using the Image Fetcher thread to do this? if (getResource() instanceof MapResource) ((MapResource)getResource()).scanForMetaData(LWImage.this, true); } */ public static void main(String args[]) throws Exception { // GUI init required for fully loading all image codecs (tiff gets left behind otherwise) // Ah: the TIFF reader in Java 1.5 apparently comes from the UI library: // [Loaded com.sun.imageio.plugins.tiff.TIFFImageReader // from /System/Library/Frameworks/JavaVM.framework/Versions/1.5.0/Classes/ui.jar] VUE.init(args); System.out.println(java.util.Arrays.asList(javax.imageio.ImageIO.getReaderFormatNames())); System.out.println(java.util.Arrays.asList(javax.imageio.ImageIO.getReaderMIMETypes())); String filename = args[0]; java.io.File file = new java.io.File(filename); System.out.println("Reading " + file); System.out.println("ImageIO.read got: " + ImageIO.read(file)); /* The below code requires the JAI libraries: // JAI (Java Advandced Imaging) libraries /System/Library/Java/Extensions/jai_core.jar /System/Library/Java/Extensions/jai_codec.jar Using this code below will also get us decoding .fpx images, tho we would need to convert it from the resulting RenderedImage / PlanarImage */ /* try { // Use the ImageCodec APIs com.sun.media.jai.codec.SeekableStream stream = new com.sun.media.jai.codec.FileSeekableStream(filename); String[] names = com.sun.media.jai.codec.ImageCodec.getDecoderNames(stream); System.out.println("ImageCodec API's found decoders: " + java.util.Arrays.asList(names)); com.sun.media.jai.codec.ImageDecoder dec = com.sun.media.jai.codec.ImageCodec.createImageDecoder(names[0], stream, null); java.awt.image.RenderedImage im = dec.decodeAsRenderedImage(); System.out.println("ImageCodec API's got RenderedImage: " + im); Object image = javax.media.jai.PlanarImage.wrapRenderedImage(im); System.out.println("ImageCodec API's got PlanarImage: " + image); } catch (Exception e) { e.printStackTrace(); } // We're not magically getting any new codec's added to ImageIO after the above code // finds the .fpx codec... System.out.println(java.util.Arrays.asList(javax.imageio.ImageIO.getReaderFormatNames())); System.out.println(java.util.Arrays.asList(javax.imageio.ImageIO.getReaderMIMETypes())); */ } // /* // * These 2 methods are used by the Preferences to set and check MaxRenderSize // */ // // TODO: no longer meaninful -- was only used to determine if the old preference // // was set to 0 size, meaning do NOT create images // public static int getMaxIconDimension() // { // return DefaultMaxDimension; // } }