org.jxmapviewer.painter.AbstractPainter Maven / Gradle / Ivy
/*
* $Id: AbstractPainter.java 4082 2011-11-15 18:39:43Z kschaefe $
*
* Copyright 2004 Sun Microsystems, Inc., 4150 Network Circle,
* Santa Clara, California 95054, U.S.A. All rights reserved.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
*/
package org.jxmapviewer.painter;
import java.awt.AlphaComposite;
import java.awt.Composite;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.image.BufferedImage;
import java.awt.image.BufferedImageOp;
import java.lang.ref.SoftReference;
import org.jxmapviewer.beans.AbstractBean;
import org.jxmapviewer.util.GraphicsUtilities;
/**
* A convenient base class from which concrete {@link Painter} implementations may
* extend. It extends {@link org.jxmapviewer.beans.AbstractBean} as a convenience for
* adding property change notification support. In addition, AbstractPainter
* provides subclasses with the ability to cacheable painting operations, configure the
* drawing surface with common settings (such as antialiasing and interpolation), and
* toggle whether a subclass paints or not via the visibility
property.
*
* Subclasses of AbstractPainter
generally need only override the
* {@link #doPaint(Graphics2D, Object, int, int)} method. If a subclass requires more control
* over whether caching is enabled, or for configuring the graphics state, then it
* may override the appropriate protected methods to interpose its own behavior.
*
* For example, here is the doPaint method of a simple Painter
that
* paints an opaque rectangle:
*
*
*
* public void doPaint(Graphics2D g, T obj, int width, int height) {
* g.setPaint(Color.BLUE);
* g.fillRect(0, 0, width, height);
* }
*
*
* @author rbair
* @param an optional configuration parameter
*/
@SuppressWarnings("nls")
public abstract class AbstractPainter extends AbstractBean implements Painter {
/**
* An enum representing the possible interpolation values of Bicubic, Bilinear, and
* Nearest Neighbor. These map to the underlying RenderingHints,
* but are easier to use and serialization safe.
*/
public enum Interpolation {
/**
* use bicubic interpolation
*/
Bicubic(RenderingHints.VALUE_INTERPOLATION_BICUBIC),
/**
* use bilinear interpolation
*/
Bilinear(RenderingHints.VALUE_INTERPOLATION_BILINEAR),
/**
* use nearest neighbor interpolation
*/
NearestNeighbor(RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR);
private Object value;
Interpolation(Object value) {
this.value = value;
}
private void configureGraphics(Graphics2D g) {
g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, value);
}
}
//--------------------------------------------------- Instance Variables
/**
* The cached image, if shouldUseCache() returns true
*/
private transient SoftReference cachedImage;
private boolean cacheCleared = true;
private boolean cacheable = false;
private boolean dirty = false;
private BufferedImageOp[] filters = new BufferedImageOp[0];
private boolean antialiasing = true;
private Interpolation interpolation = Interpolation.NearestNeighbor;
private boolean visible = true;
private boolean inPaintContext;
/**
* Creates a new instance of AbstractPainter.
*/
public AbstractPainter() { }
/**
* Creates a new instance of AbstractPainter.
* @param cacheable indicates if this painter should be cacheable
*/
public AbstractPainter(boolean cacheable) {
setCacheable(cacheable);
}
/**
* A defensive copy of the Effects to apply to the results
* of the AbstractPainter's painting operation. The array may
* be empty but it will never be null.
* @return the array of filters applied to this painter
*/
public final BufferedImageOp[] getFilters() {
BufferedImageOp[] results = new BufferedImageOp[filters.length];
System.arraycopy(filters, 0, results, 0, results.length);
return results;
}
/**
* A convenience method for specifying the filters to use based on
* BufferedImageOps. These will each be individually wrapped by an ImageFilter
* and then setFilters(Effect... filters) will be called with the resulting
* array
*
*
* @param effects the BufferedImageOps to wrap as filters
*/
public void setFilters(BufferedImageOp ... effects) {
if (effects == null) effects = new BufferedImageOp[0];
BufferedImageOp[] old = getFilters();
this.filters = new BufferedImageOp[effects.length];
System.arraycopy(effects, 0, this.filters, 0, this.filters.length);
setDirty(true);
firePropertyChange("filters", old, getFilters());
}
/**
* Returns if antialiasing is turned on or not. The default value is true.
* This is a bound property.
* @return the current antialiasing setting
*/
public boolean isAntialiasing() {
return antialiasing;
}
/**
* Sets the antialiasing setting. This is a bound property.
* @param value the new antialiasing setting
*/
public void setAntialiasing(boolean value) {
boolean old = isAntialiasing();
antialiasing = value;
if (old != value) setDirty(true);
firePropertyChange("antialiasing", old, isAntialiasing());
}
/**
* Gets the current interpolation setting. This property determines if interpolation will
* be used when drawing scaled images. @see java.awt.RenderingHints.KEY_INTERPOLATION.
* @return the current interpolation setting
*/
public Interpolation getInterpolation() {
return interpolation;
}
/**
* Sets a new value for the interpolation setting. This setting determines if interpolation
* should be used when drawing scaled images. @see java.awt.RenderingHints.KEY_INTERPOLATION.
* @param value the new interpolation setting
*/
public void setInterpolation(Interpolation value) {
Object old = getInterpolation();
this.interpolation = value == null ? Interpolation.NearestNeighbor : value;
if (old != value) setDirty(true);
firePropertyChange("interpolation", old, getInterpolation());
}
/**
* Gets the visible property. This controls if the painter should
* paint itself. It is true by default. Setting visible to false
* is good when you want to temporarily turn off a painter. An example
* of this is a painter that you only use when a button is highlighted.
*
* @return current value of visible property
*/
public boolean isVisible() {
return this.visible;
}
/**
* Sets the visible property. This controls if the painter should
* paint itself. It is true by default. Setting visible to false
* is good when you want to temporarily turn off a painter. An example
* of this is a painter that you only use when a button is highlighted.
*
* @param visible New value of visible property.
*/
public void setVisible(boolean visible) {
boolean old = isVisible();
this.visible = visible;
if (old != visible) setDirty(true); //not the most efficient, but I must do this otherwise a CompoundPainter
//or other aggregate painter won't know that it is now invalid
//there might be a tricky solution but that is a performance optimization
firePropertyChange("visible", old, isVisible());
}
/**
* Gets whether this AbstractPainter
can be cached as an image.
* If caching is enabled, then it is the responsibility of the developer to
* invalidate the painter (via {@link #clearCache}) if external state has
* changed in such a way that the painter is invalidated and needs to be
* repainted.
*
* @return whether this is cacheable
*/
public boolean isCacheable() {
return cacheable;
}
/**
* Sets whether this AbstractPainter
can be cached as an image.
* If true, this is treated as a hint. That is, a cacheable may or may not be used.
* The {@link #shouldUseCache} method actually determines whether the cacheable is used.
* However, if false, then this is treated as an absolute value. That is, no
* cacheable will be used.
*
* If set to false, then #clearCache is called to free system resources.
*
* @param cacheable the cache flag
*/
public void setCacheable(boolean cacheable) {
boolean old = isCacheable();
this.cacheable = cacheable;
firePropertyChange("cacheable", old, isCacheable());
if (!isCacheable()) {
clearCache();
}
}
/**
* Call this method to clear the cacheable. This may be called whether there is
* a cacheable being used or not. If cleared, on the next call to paint
,
* the painting routines will be called.
*
* SubclassesIf overridden in subclasses, you
* must call super.clearCache, or physical
* resources (such as an Image) may leak.
*/
public void clearCache() {
BufferedImage cache = cachedImage == null ? null : cachedImage.get();
if (cache != null) {
cache.flush();
}
cacheCleared = true;
if (!isCacheable()) {
cachedImage = null;
}
}
// /**
// * Only made package private for testing. Don't call this method outside
// * of this class! This is NOT a bound property
// */
// private boolean isCacheCleared() {
// return cacheCleared;
// }
/**
* Called to allow Painter
subclasses a chance to see if any state
* in the given object has changed from the last paint operation. If it has, then
* the Painter
has a chance to mark itself as dirty, thus causing a
* repaint, even if cached.
*
* @param object the object to validate
*/
protected void validate(T object) { /* do nothing */ }
/**
* Ye olde dirty bit. If true, then the painter is considered dirty and in need of
* being repainted. This is a bound property.
*
* @return true if the painter state has changed and the painter needs to be
* repainted.
*/
protected boolean isDirty() {
return dirty;
}
/**
* Sets the dirty bit. If true, then the painter is considered dirty, and the cache
* will be cleared. This property is bound.
*
* @param d whether this Painter
is dirty.
*/
protected void setDirty(boolean d) {
boolean old = isDirty();
this.dirty = d;
firePropertyChange("dirty", old, isDirty());
if (isDirty()) {
clearCache();
}
}
boolean isInPaintContext() {
return inPaintContext;
}
void setInPaintContext(boolean inPaintContext) {
this.inPaintContext = inPaintContext;
}
/**
* Returns true if the painter should use caching. This method allows subclasses to
* specify the heuristics regarding whether to cache or not. If a Painter
* has intelligent rules regarding painting times, and can more accurately indicate
* whether it should be cached, it could implement that logic in this method.
*
* @return whether or not a cache should be used
*/
protected boolean shouldUseCache() {
return isCacheable() || filters.length > 0; //NOTE, I can only do this because getFilters() is final
}
/**
* This method is called by the paint
method prior to
* any drawing operations to configure the drawing surface. The default
* implementation sets the rendering hints that have been specified for
* this AbstractPainter
.
*
* This method can be overridden by subclasses to modify the drawing
* surface before any painting happens.
*
* @param g the graphics surface to configure. This will never be null.
* @see #paint(Graphics2D, Object, int, int)
*/
protected void configureGraphics(Graphics2D g) {
//configure antialiasing
if(isAntialiasing()) {
g.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON);
} else {
g.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_OFF);
}
getInterpolation().configureGraphics(g);
}
/**
* Subclasses must implement this method and perform custom painting operations
* here.
* @param width the width
* @param height the height
* @param g The Graphics2D object in which to paint
* @param object an optional configuration parameter
*/
protected abstract void doPaint(Graphics2D g, T object, int width, int height);
@Override
public final void paint(Graphics2D g, T obj, int width, int height) {
if (g == null) {
throw new NullPointerException("The Graphics2D must be supplied");
}
if(!isVisible() || width < 1 || height < 1) {
return;
}
configureGraphics(g);
//paint to a temporary image if I'm caching, or if there are filters to apply
if (shouldUseCache() || filters.length > 0) {
validate(obj);
BufferedImage cache = cachedImage == null ? null : cachedImage.get();
boolean invalidCache = null == cache ||
cache.getWidth() != width ||
cache.getHeight() != height;
if (cacheCleared || invalidCache || isDirty()) {
//rebuild the cacheable. I do this both if a cacheable is needed, and if any
//filters exist. I only *save* the resulting image if caching is turned on
if (invalidCache) {
cache = GraphicsUtilities.createCompatibleTranslucentImage(width, height);
}
if (cache != null) {
Graphics2D gfx = cache.createGraphics();
try {
gfx.setClip(0, 0, width, height);
if (!invalidCache) {
// If we are doing a repaint, but we didn't have to
// recreate the image, we need to clear it back
// to a fully transparent background.
Composite composite = gfx.getComposite();
gfx.setComposite(AlphaComposite.Clear);
gfx.fillRect(0, 0, width, height);
gfx.setComposite(composite);
}
configureGraphics(gfx);
doPaint(gfx, obj, width, height);
} finally {
gfx.dispose();
}
if (!isInPaintContext()) {
for (BufferedImageOp f : getFilters()) {
cache = f.filter(cache, null);
}
}
//only save the temporary image as the cacheable if I'm caching
if (shouldUseCache()) {
cachedImage = new SoftReference(cache);
cacheCleared = false;
}
}
}
g.drawImage(cache, 0, 0, null);
} else {
//can't use the cacheable, so just paint
doPaint(g, obj, width, height);
}
//painting has occured, so restore the dirty bit to false
setDirty(false);
}
}