/* * 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 static tufts.Util.*; import edu.tufts.vue.preferences.implementations.BooleanPreference; import java.awt.Color; import java.awt.Shape; import java.awt.Graphics; import java.awt.Graphics2D; import java.awt.Rectangle; import java.awt.AlphaComposite; import java.awt.geom.Rectangle2D; import java.awt.geom.AffineTransform; import java.awt.RenderingHints; import static java.awt.RenderingHints.*; import static java.lang.Boolean.*; import com.google.common.collect.Multiset; /** * Includes a Graphics2D context and adds VUE specific flags and helpers * for rendering a tree of LWComponents. * * @version $Revision: 1.72 $ / $Date: 2010-02-03 19:17:40 $ / $Author: mike $ * @author Scott Fraize * */ public final class DrawContext { public final Graphics2D g; public final double zoom; public final float offsetX; public final float offsetY; private static final String QUALITY_DRAFT = "draft"; private static final String QUALITY_NORMAL = "normal"; private static final String QUALITY_PRINT = "print"; private int index; private int maxLayer = Short.MAX_VALUE; // don't draw layers above this level private boolean disableAntiAlias; private boolean isInteractive; private boolean isBlackWhiteReversed; private boolean isPresenting; private boolean isDrawingPathways = true; public final Rectangle frame; // if we have the pixel dimensions of the surface we're drawing on, they go here private Object quality = QUALITY_NORMAL; public final LWComponent focal; LWComponent focused; private float alpha = 1f; //private VueTool activeTool; private final Shape rawClip; private final AffineTransform rawTransform; private final AffineTransform mapTransform; //private AffineTransform lastTransform; private Rectangle2D masterClipRect; // for drawing map nodes private Color fillColor; public LWComponent skipDraw; private boolean isClipOptimized = true; // todo: rename isPaintOptimized, and make the default false private boolean isAnimating; private boolean isBrowsing = false; // todo: consider including a Conatiner arg in here, for // MapViewer, etc. And replace zoom with a getZoom // that grabs transform scale value. // todo: move coord mappers from MapViewer to here? public DrawContext(Graphics _g, double zoom, float offsetX, float offsetY, Rectangle frame, LWComponent focal, boolean absoluteLinks) { this.g = (Graphics2D) _g; this.zoom = zoom; this.offsetX = offsetX; this.offsetY = offsetY; this.frame = frame; this.focal = focal; this.rawClip = g.getClip(); //this.g.translate(frame.x, frame.y); this.rawTransform = g.getTransform(); this.g.translate(offsetX, offsetY); this.g.scale(zoom, zoom); this.mapTransform = g.getTransform(); setMasterClip(g.getClip()); if (DEBUG.PAINT) out("CONSTRUCTED"); //setMasterClip(rawClip = g.getClip()); //this.drawAbsoluteLinks = absoluteLinks; //setPrioritizeSpeed(true); //disableAntiAlias(true); } public DrawContext(Graphics g, LWComponent focal) { this(g, 1.0, 0, 0, (Rectangle) null, focal, false); //setBackgroundFill(focal.getRenderFillColor(null)); // caller should do manually } public DrawContext(Graphics g, double zoom) { this(g, zoom, 0, 0, (Rectangle) null, (LWComponent) null, false); } public DrawContext(Graphics g) { this(g, 1.0); } private DrawContext lastPush; public DrawContext push() { if (lastPush != null) Util.printStackTrace("Unpopped DC: " + lastPush); return lastPush = create(); } public void pop() { lastPush.dispose(); lastPush = null; } public void dispose() { // in case we try to use it after this, this should ensure exceptions: isClipOptimized = true; masterClipRect = null; g.dispose(); } public void fillBackground(Color c) { if (fillColor != null) Util.printStackTrace(this + " already filled with " + fillColor); setBackgroundFill(c); g.setColor(c); g.fill(g.getClipBounds()); } /** Fill the given shape will the given color: will be a noop if the fill color matches the current BG fill */ public void fillArea(final Shape shape, final Color fill) { if (fill == null || fill.equals(fillColor)) { if (DEBUG.PRESENT && DEBUG.META) Util.printStackTrace("skipping fillArea matching existing bg fill " + fill + " " + fmt(shape)); return; } g.setColor(fill); g.fill(shape); } public Color getBackgroundFill() { return fillColor; } public void setBackgroundFill(Color c) { //if (DEBUG.IMAGE) out("setFill: " + c); fillColor = c; } /** set up for drawing a model: adjust to the current zoom and offset. * MapViewer, MapPanner, VueTool, etc, to use.*/ // todo: change to single setMapDrawing(boolean) /* public void setMapDrawing() { if (rawTransform != null) throw new Error("DrawContext: map paramaters already established"); //if (!inMapDraw) { rawTransform = g.getTransform(); g.translate(offsetX, offsetY); g.scale(zoom, zoom); mapTransform = g.getTransform(); setMasterClip(g.getClip()); //System.out.println("DC SCALE TO " + zoom); //System.out.println("DC SCALE TO " + g.getTransform()); //inMapDraw = true; //} } public void resetMapDrawing() { if (mapTransform != null) g.setTransform(mapTransform); else throw new Error("DrawContext: initial map transform not established"); } public void setRawDrawing() { //if (inMapDraw) { if (rawTransform == null) throw new IllegalStateException("attempt to revert to raw draw in a derivative DrawContext"); //System.out.println("DC REVER TO " + savedTransform); g.setTransform(rawTransform); //setMasterClip(rawClip); // inMapDraw = false; // } } */ public boolean isAnimating() { return isAnimating; } public void setAnimating(boolean animating) { isAnimating = animating; // if (animating) // quality = QUALITY_DRAFT; // else // quality = QUALITY_NORMAL; } public void setMapDrawing() { isClipOptimized = true; g.setTransform(mapTransform); } public void setRawDrawing() { isClipOptimized = false; g.setTransform(rawTransform); } public void setFrameDrawing() { setRawDrawing(); g.translate(frame.x, frame.y); } public void setClipOptimized(boolean clipOptimized) { isClipOptimized = clipOptimized; } /** * @return true if we can do clip bounds checking optimizations to * know if we can skip drawing an LWComponent entirely. Only * works if we're drawing "proper" children of a current map. * This is as opposed to special decorations that happened to be * LWComponents that we want to draw no matter what what their * current location is (often, always 0,0). E.g., a master slide * background, on-map slide icons, presentation navigation nodes, * etc. */ public boolean isClipOptimized() { if (DEBUG.CONTAINMENT && DEBUG.META && DEBUG.WORK) // TODO: temporary weird case return false; else return isClipOptimized; } public void setMasterClip(Shape clip) { g.setClip(clip); if (clip instanceof Rectangle2D) { if (DEBUG.PAINT) out("SET MASTER CLIP RECT2D " + fmt(clip)); masterClipRect = (Rectangle2D) clip; //masterClipRect = (Rectangle2D) ((Rectangle2D)clip).clone(); //if (DEBUG.PAINT) out("SET MASTER CLIP RECT2D DONE " + fmt(masterClipRect)); } else { // we've set the shaped clip in the gc, now extract the master clip rectangle from the gc masterClipRect = g.getClipBounds(); if (DEBUG.PAINT || DEBUG.CONTAINMENT) { out("SET SHAPE CLIP: " + fmt(clip)); //System.out.println("MASTER CLIP RECT2D: " + Util.out(masterClipRect)); } } // if (DEBUG.PAINT || (DEBUG.CONTAINMENT&&DEBUG.META) || DEBUG.PRESENT) // out("SET MASTER CLIP RECT2D " + fmt(masterClipRect)); } public Rectangle2D getMasterClipRect() { if (masterClipRect == null) { Util.printStackTrace(this + " null masterClipRect!"); masterClipRect = this.g.getClipBounds(); } //return (Rectangle2D) ((Rectangle2D)masterClipRect).clone(); return masterClipRect; } public void setMaxLayer(int layer) { maxLayer = layer; } public int getMaxLayer() { return maxLayer; //return 99; } /* public setFrame(Rectangle frame) { this.frame = frame; } */ public Rectangle getFrame() { return new Rectangle(frame); } // public void setActiveTool(VueTool tool) { // activeTool = tool; // } // public VueTool getActiveTool() { // return activeTool; // } public void setAlpha(double p_alpha, int alphaRule) { this.alpha = (float) p_alpha; if (alpha == 1f) g.setComposite(AlphaComposite.Src); else g.setComposite(AlphaComposite.getInstance(alphaRule, this.alpha)); // todo: cache the alpha instance } public void setAlpha(double alpha) { setAlpha(alpha, AlphaComposite.SRC_OVER); } public float getAlpha() { return this.alpha; } public static final String[] AlphaRuleNames = { // Index based on coded values in java.awt.AlphaComposite.java @version 10 Feb 1997: "<none>", "CLEAR", "SRC", "SRC_OVER", "DST_OVER", "SRC_IN", "DST_IN", "SRC_OUT", "DST_OUT", "DST", "SRC_ATOP", "DST_ATOP", "XOR" }; public void checkComposite(LWComponent c) { if (false&&alpha != 1f) { // if we're going to do a non-opaque fill during a general transparent // rendering situation, change temporarily to the SRC_OVER rule instead of // SRC, so that what's under the translucent node will show through. If we // just left it SRC, color values that had an alpha channel would end up // blowing away what's underneath them. final Color fill = c.getRenderFillColor(this); // TODO: images with transparency in them may need special handling // At the moment, any time we draw with a global transparency, // images with transparency are filling their background with black! if (c instanceof LWImage) { // not perfect, but helps sometimes: setAlpha(alpha, AlphaComposite.SRC_ATOP); //System.out.println("IMAGE COMPOSITE " + c); } else if (fill != null && fill.getAlpha() != 255) { // merge with background: setAlpha(alpha, AlphaComposite.SRC_OVER); //System.out.println("COMPOSITE: SRC_OVER (has fill with partial transparency) " + c); } else { // draw on top of background: //System.out.println("COMPOSITE: SRC (has no fill or opaque fill) " + c); setAlpha(alpha, AlphaComposite.SRC); } } } //public float getAlpha() { return mAlpha; } /** * Mark us as rendering for interactive usage: e.g., selection will be drawn. */ public void setInteractive(boolean t) { isInteractive = t; } public boolean isInteractive() { return isInteractive; } public boolean isBlackWhiteReversed() { return isBlackWhiteReversed; } public void setBlackWhiteReversed(boolean t) { isBlackWhiteReversed = t; } public void setPresenting(boolean t) { isPresenting = t; } public boolean isPresenting() { return isPresenting; } /** @return true for special temporary state alternate indications */ public void setIndicated(LWComponent c) { focused = c; // using focused field for now (overloaded) } public boolean isIndicated(LWComponent c) { return focused == c; } public boolean hasIndicated() { return focused != null; } public LWComponent getIndicated() { return focused; } public boolean drawPathways() { return isDrawingPathways; //return !isPresenting() && focal instanceof LWMap; // return !isFocused && !isPresenting(); } public void setDrawPathways(boolean drawPathways) { isDrawingPathways = drawPathways; } public boolean isFocused() { return focal instanceof LWMap == false; } /** @return true of Level-Of-Detail rendering is enabled/permitted */ public boolean isLODEnabled() { // todo: this is inferred -- should have a separate bit for this return isInteractive() || isDraftQuality(); } public void disableAntiAlias(boolean disable) { this.disableAntiAlias = disable; if (disable) setAntiAlias(false); } /* passthru to Graphcs.setColor. This method available for override public void setColor(Color c) { g.setColor(c); }*/ /** Turn on or off text & shape anti-aliasing */ public void setAntiAlias(boolean on) { if (disableAntiAlias) { setAliasQuality(g, FALSE); setAliasTextQuality(g, FALSE); } else { setAliasQuality(g, on); setAliasTextQuality(g, on); } } public static final Boolean DEFAULT = new Boolean(false); // true/false not relevant public static final Boolean QUALITY = Boolean.TRUE; public static final Boolean SPEED = Boolean.FALSE; public static final Boolean INTERPOLATION_SPEED = SPEED; public static final Boolean INTERPOLATION_BETTER = DEFAULT; public static final Boolean INTERPOLATION_BEST = QUALITY; /** "normal" quality */ public void setInteractiveQuality() { setImageQuality(g, isImageQualityRequested() ? QUALITY : SPEED); //setImageQuality(g, QUALITY); //setImageQuality(g, DEFAULT); // even best quality under Java 1.6 appears to be crappy compared to best under 1.5 on Mac OS X 10.5.8 //setInterpolation(g, INTERPOLATION_SPEED); //setInterpolation(g, INTERPOLATION_BETTER); //setInterpolation(g, INTERPOLATION_BEST); setAlphaQuality(g, DEFAULT); setColorQuality(g, DEFAULT); setFontQuality(g, QUALITY); //setFontQuality(g, DEFAULT); setAntiAlias(true); //setDitherQuality(g, TRUE); quality = QUALITY_NORMAL; } public boolean isDraftQuality() { return quality == QUALITY_DRAFT; } /** fast rendering with bit set for renderers to check */ public void setDraftQuality() { setFastQuality(); quality = QUALITY_DRAFT; } /** "fast" quality */ private void setFastQuality() { setInterpolation(g, INTERPOLATION_SPEED); setImageQuality(g, FALSE); setAlphaQuality(g, FALSE); setColorQuality(g, FALSE); setFontQuality(g, FALSE); setAntiAlias(true); // never turn off anti-alias } public void setPrintQuality() { setInterpolation(g, INTERPOLATION_BEST); setImageQuality(g, TRUE); setAlphaQuality(g, TRUE); setColorQuality(g, TRUE); // improve quality of printed text (high DPI's are needed to take advantage of fractional metrics) setFontQuality(g, TRUE); setAntiAlias(true); quality = QUALITY_PRINT; } public boolean isPrintQuality() { return quality == QUALITY_PRINT; } public boolean isNormalQuality() { return quality == QUALITY_NORMAL; } public void setAnimatingQuality() { setFastQuality(); } public void setFractionalFontMetrics(boolean on) { setFontQuality(g, on); } // public void setPrioritizeSpeed(boolean speed) // { // setPrioritizeQuality(!speed); // } /** set a stroke width that stays constant on-screen at given * width independent of any current scaling (presuming * scaling is same in X/Y direction's -- only tracks X scale factor). * NOTE: doesn't take into account stroke style (dashing) -- always produces * solid stroke. */ public void setAbsoluteStroke(double width) { // double scale = getAbsoluteScale(); // if (scale <= 0) // scale = 1; // this.g.setStroke(new java.awt.BasicStroke((float) (width / scale))); setAbsoluteStroke(this.g, width); } public static void setAbsoluteStroke(Graphics2D g, double width) { double scale = getAbsoluteScale(g); if (scale <= 0) scale = 1; g.setStroke(new java.awt.BasicStroke((float) (width / scale))); } public double getAbsoluteScale() { return this.g.getTransform().getScaleX(); } public static double getAbsoluteScale(Graphics2D g) { return g.getTransform().getScaleX(); } public void setAbsoluteScale(double absScale) { final double adjScale = (1 / g.getTransform().getScaleX()) * absScale; if (adjScale > 0) g.scale(adjScale, adjScale); } /** * An arbitrary counter that can be set & read. */ public void setIndex(int i) { this.index = i; } /** * An arbitrary counter that can be set & read. */ public int getIndex() { return this.index; } // todo: make this safer public void setAbsoluteDrawing(boolean unZoom) { if (unZoom) g.scale(1/zoom, 1/zoom); else g.scale(zoom, zoom); } public DrawContext create() { return new DrawContext(this); } public String toString() { //return String.format("DrawContext@%x[zoom=%.2f mapOffset=%.1f,%.1f focal=%s]", return String.format("DrawContext@%06X[%.1f%% %s %s%s]", hashCode(), zoom * 100, //offsetX, offsetY, focal == null ? "null-focal" : focal.getUniqueComponentTypeLabel(), fmt(masterClipRect), isClipOptimized ? " CLIPPING" : " DRAW-ALL" ); } public static void setImageQuality(Graphics g, Boolean on) { setQuality(g,KEY_RENDERING, on, VALUE_RENDER_DEFAULT, VALUE_RENDER_QUALITY, VALUE_RENDER_SPEED); } public static void setInterpolation(Graphics g, Boolean on) { setQuality(g,KEY_INTERPOLATION, on, VALUE_INTERPOLATION_BILINEAR, // better ("default") VALUE_INTERPOLATION_BICUBIC, // best & slowest ("quality") VALUE_INTERPOLATION_NEAREST_NEIGHBOR); // fastest ("speed") } public static void setAlphaQuality(Graphics g, Boolean on) { setQuality(g,KEY_ALPHA_INTERPOLATION, on, VALUE_ALPHA_INTERPOLATION_DEFAULT, VALUE_ALPHA_INTERPOLATION_QUALITY, VALUE_ALPHA_INTERPOLATION_SPEED); } public static void setColorQuality(Graphics g, Boolean on) { setQuality(g,KEY_COLOR_RENDERING, on, VALUE_COLOR_RENDER_DEFAULT, VALUE_COLOR_RENDER_QUALITY, VALUE_COLOR_RENDER_SPEED); } public static void setFontQuality(Graphics g, Boolean on) { setQuality(g,KEY_FRACTIONALMETRICS, on, VALUE_FRACTIONALMETRICS_DEFAULT, VALUE_FRACTIONALMETRICS_ON, VALUE_FRACTIONALMETRICS_OFF); } public static void setAliasQuality(Graphics g, Boolean on) { setQuality(g,KEY_ANTIALIASING, on, VALUE_ANTIALIAS_DEFAULT, VALUE_ANTIALIAS_ON, VALUE_ANTIALIAS_OFF); } public static void setAliasTextQuality(Graphics g, Boolean on) { setQuality(g,KEY_TEXT_ANTIALIASING, on, VALUE_TEXT_ANTIALIAS_DEFAULT, VALUE_TEXT_ANTIALIAS_ON, VALUE_TEXT_ANTIALIAS_OFF); } public static void setDitherQuality(Graphics g, Boolean on) { setQuality(g,KEY_DITHERING, on, VALUE_DITHER_DISABLE, VALUE_DITHER_ENABLE, VALUE_DITHER_DEFAULT); } private static void setQuality (final Graphics g, final RenderingHints.Key key, final Boolean on, final Object defaultValue, final Object qualityValue, final Object speedValue) { final Object hint; if (on == DEFAULT) hint = defaultValue; else if (on) hint = qualityValue; else hint = speedValue; if (DEBUG.PAINT) { System.out.format(TERM_PURPLE + "%s %7s for %-41s = %s\n" + TERM_CLEAR, Util.tag(g), on==DEFAULT?"DEFAULT": (on ? "quality" : "speed"), '[' + key.toString() + ']', Util.tags(hint.toString())); } ((Graphics2D)g).setRenderingHint(key, hint); } protected void out(String s) { System.err.println(Util.TERM_PURPLE + this + Util.TERM_CLEAR + " " + s); } private static void msg(String s) { System.err.println(Util.TERM_PURPLE + s + Util.TERM_CLEAR); } public DrawContext(DrawContext dc) { this(dc, dc.focal); } // todo: replace with a faster clone op? public DrawContext(DrawContext dc, LWComponent newFocal) { //System.out.println("transform before dupe: " + dc.g.getTransform()); this.g = (Graphics2D) dc.g.create(); //this.g = dc.g; // This helps itext (tho is not a workaround) -- it looks like the GC provided by // itext doesn't handle create properly -- it almost looks as if a fill is required // every time a new GC is created for fill's in subsequent child fill's or even text to work // (tho stroking does seem to work) -- e.g., a group with no fill at all will // not fill any of it's children, or draw any text -- you only see strokes. // Setting a fill doesn't always guarantee to fix this tho... vanilla // side (non map-view) content seems to be the worst. //g.setColor(new java.awt.Color(4,4,4,4)); // helps //g.setColor(new java.awt.Color(0,0,0,0)); // less help //g.fillRect(-Short.MAX_VALUE/2, -Short.MAX_VALUE/2, Short.MAX_VALUE, Short.MAX_VALUE); //System.out.println("transform after dupe: " + g.getTransform()); this.zoom = dc.zoom; this.offsetX = dc.offsetX; this.offsetY = dc.offsetY; this.disableAntiAlias = dc.disableAntiAlias; this.index = dc.index; this.isInteractive = dc.isInteractive; this.quality = dc.quality; this.isBlackWhiteReversed = dc.isBlackWhiteReversed; this.isPresenting = dc.isPresenting; this.isDrawingPathways = dc.isDrawingPathways; //this.activeTool = dc.activeTool; //this.inMapDraw = dc.inMapDraw; this.mapTransform = dc.mapTransform; this.frame = dc.frame; this.focal = newFocal; this.alpha = dc.alpha; //this.drawAbsoluteLinks = dc.drawAbsoluteLinks; this.maxLayer = dc.maxLayer; this.rawClip = dc.rawClip; this.rawTransform = dc.rawTransform; this.masterClipRect = dc.masterClipRect; this.skipDraw = dc.skipDraw; this.fillColor = dc.fillColor; this.isClipOptimized = dc.isClipOptimized; this.isAnimating = dc.isAnimating; this.focused = dc.focused; this.isBrowsing = dc.isBrowsing; if (DEBUG.PAINT&&DEBUG.META) out("CLONE of " + dc); //out("CLONED: " + Util.tag(masterClipRect) + " from " + dc); //Util.printClassTrace("tufts.vue", "CLONE " + this); //this.mAlpha = dc.mAlpha; } private static final Multiset DebugRecording = com.google.common.collect.HashMultiset.create(); public static void recordDebug(Object value) { DebugRecording.add(value); } public static void recordDebug(LWComponent c) { //final String type = c.getClass().getName(); final String type = c.getComponentTypeLabel(); if (c.getTypeToken() != null && c.getTypeToken() != c.getClass()) recordDebug(type + ":" + c.getTypeToken()); else recordDebug(type); } public static String getDebug() { return DebugRecording.toString(); } public static void clearDebug() { DebugRecording.clear(); } private final static boolean PlatformQualityIsSlow; public static boolean drawingMayBeSlow(final LWComponent focal) { // could check focal for the presence of images //if (true) return false; if (PlatformQualityIsSlow && isImageQualityRequested()) return focal instanceof LWMap; else return false; } //private final static BooleanPreference ImageQualityPreference; public static boolean isImageQualityRequested() { //return ImageQualityPreference != null && ImageQualityPreference.isTrue(); //return ImageQualityPreference.isTrue(); return true; } public boolean isBrowsing() { return isBrowsing; } public void setBrowsing(boolean value) { isBrowsing = value; } static { if (Util.isMacLeopard() && Util.getJavaVersion() < 1.6f) PlatformQualityIsSlow = true; // slow, but the only one truly effective! else PlatformQualityIsSlow = false; // ImageQualityPreference = BooleanPreference.create // (edu.tufts.vue.preferences.PreferenceConstants.MAPDISPLAY_CATEGORY, // "imageQuality", // VueResources.getString("preference.imageQuality.title", "Image Quality"), // VueResources.getString("preference.imageQuality.description", // "Disabling this will make VUE faster when working on maps with images" // ), // Boolean.TRUE, // true); } // private final static BooleanPreference ImageQualityPreference = BooleanPreference.create( // edu.tufts.vue.preferences.PreferenceConstants.MAPDISPLAY_CATEGORY, // "imageQuality", // VueResources.getString("preference.imageQuality.title", "Image Quality"), // VueResources.getString("preference.imageQuality.description", // "Disabling this will make VUE faster when working on maps with images" // ), // Boolean.TRUE, // true); // @Override // public final Object clone() { // // final DrawContext dc = new DrawContext(g); // // return dc; // try { // final DrawContext dc = (DrawContext) super.clone(); // dc.focal = null; // return dc; // } catch (CloneNotSupportedException e) { // e.printStackTrace(); // } // return null; // } }