/*
* Geotoolkit.org - An Open Source Java GIS Toolkit
* http://www.geotoolkit.org
*
* (C) 2006-2012, Open Source Geospatial Foundation (OSGeo)
* (C) 2009-2012, Geomatys
*
* 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;
* version 2.1 of the License.
*
* 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.
*/
package org.geotoolkit.image;
import java.awt.Image;
import java.awt.image.*;
import java.awt.Transparency;
import java.awt.RenderingHints;
import java.awt.HeadlessException;
import java.awt.color.ColorSpace;
import java.lang.reflect.InvocationTargetException;
import javax.media.jai.*;
import javax.media.jai.operator.*;
import com.sun.media.jai.util.ImageUtil;
import org.opengis.coverage.PaletteInterpretation;
import org.geotoolkit.lang.Debug;
import org.geotoolkit.factory.Hints;
import org.geotoolkit.image.color.ColorModels;
import org.geotoolkit.image.internal.ImageUtilities;
import org.geotoolkit.image.color.ColorUtilities;
import static java.awt.image.DataBuffer.TYPE_BYTE;
import static org.apache.sis.util.ArgumentChecks.ensureNonNull;
import static org.apache.sis.util.collection.Containers.isNullOrEmpty;
/**
* Provides convenience methods for describing an image. This base class is "read only" in that
* it doesn't provide any operation that may change the pixel values. However subclasses like
* {@link ImageWorker} may applied JAI operations on the image.
*
* @author Martin Desruisseaux (Geomatys)
* @author Simone Giannecchini (Geosolutions)
* @author Bryce Nordgren
* @version 3.00
*
* @since 2.3
* @module
*
* @deprecated This class is a legacy from old days and has never been seriously used in the
* Geotoolkit.org library. Its work is quite arbitrary, so we are probably better
* to let users do their work in their own way.
*/
@Deprecated
public class ImageInspector {
/**
* An additional {@code PaletteInterpretation} code representing the IHS (Intensity, Hue,
* Saturation) color space. This color space is also known as HSI or HIS and is implemented
* in <cite>Java Advanced Imaging</cite> by the {@link IHSColorSpace} class.
*
* @since 3.00
*/
public static final PaletteInterpretation IHS = PaletteInterpretation.valueOf("IHS");
/**
* If {@link Boolean#TRUE TRUE}, the RGB values of fully transparent pixels will not be taken
* in account when deciding if an Index Color Model is gray scale. Fully transparent pixels
* are the ones having an {@linkplain IndexColorModel#getAlpha(int) alpha} value of 0.
* <p>
* The default value is {@link Boolean#TRUE}.
*
* @see #isGrayScale
* @see #setRenderingHint
*
* @since 3.00
*/
public static final Hints.Key IGNORE_FULLY_TRANSPARENT_PIXELS = new Hints.Key(Boolean.class);
/**
* The image property name generated by {@link ExtremaDescriptor}.
*/
private static final String EXTREMA = "extrema";
/**
* The image specified by the user at construction time, or last time
* {@link #invalidateStatistics} were invoked. The {@link #getComputedProperty}
* method will not search a property pass this point.
*/
private RenderedImage inheritanceStopPoint;
/**
* The image on which operation are appiled. Subclasses may replace the value of this
* field by the operation results every time a new operation is applied.
*/
protected RenderedImage image;
/**
* The region of interest, or {@code null} if none.
*/
private ROI roi;
/**
* The rendering hints to provide to all image operators. Additional hints may
* be set (in a separated {@link RenderingHints} object) for particular images.
*/
private RenderingHints commonHints;
/**
* Non-zeros if {@link ImageWorker} is performing an intermediate computation step.
* This counter is incremented everytime {@code enableTileCache(false)} is invoked,
* and decremented every time {@code enableTileCache(true)} is invoked.
*/
private int tileCacheDisabled;
/**
* An tile cache that performs do caching. Created only when needed.
*/
private transient TileCache nullCache;
/**
* Creates a new {@code ImageInspector} for the specified image.
*
* @param image The source image.
*/
public ImageInspector(final RenderedImage image) {
ensureNonNull("image", image);
inheritanceStopPoint = this.image = image;
}
/**
* Creates a new image descriptor initialized to the same image and hints than the given
* descriptor.
*/
ImageInspector(final ImageInspector base) {
this(base.image);
if (!isNullOrEmpty(base.commonHints)) {
commonHints = new RenderingHints(null);
commonHints.add(base.commonHints);
}
}
/**
* Sets the image on which future operations will be applied.
*
* @param image The new source image.
*/
public void setImage(final RenderedImage image) {
ensureNonNull("image", image);
inheritanceStopPoint = this.image = image;
}
/**
* Returns the current {@linkplain #image}.
*
* @return The rendered image.
*
* @see #getBufferedImage
* @see #getPlanarImage
* @see #getRenderedOperation
* @see #getImageAsROI
*/
public RenderedImage getRenderedImage() {
return image;
}
/**
* Returns the current {@linkplain #image} as a buffered image.
*
* @return The buffered image.
*
* @see #getRenderedImage
* @see #getPlanarImage
* @see #getRenderedOperation
* @see #getImageAsROI
*
* @since 2.5
*/
public BufferedImage getBufferedImage() {
if (image instanceof BufferedImage) {
return (BufferedImage) image;
} else {
return getPlanarImage().getAsBufferedImage();
}
}
/**
* Returns the {@linkplain #getRenderedImage rendered image} as a planar image.
*
* @return The planar image.
*
* @see #getRenderedImage
* @see #getRenderedOperation
* @see #getImageAsROI
*/
public PlanarImage getPlanarImage() {
return PlanarImage.wrapRenderedImage(getRenderedImage());
}
/**
* Returns the {@linkplain #getRenderedImage rendered image} as a rendered operation.
*
* @return The rendered operation.
*
* @see #getRenderedImage
* @see #getPlanarImage
* @see #getImageAsROI
*/
public RenderedOp getRenderedOperation() {
final RenderedImage image = getRenderedImage();
if (image instanceof RenderedOp) {
return (RenderedOp) image;
}
return NullDescriptor.create(image, getRenderingHints());
}
/**
* Returns a {@linkplain ROI Region Of Interest} built from a
* {@linkplain ImageWorker#binarize(boolean) binarized} version of the current
* {@linkplain #getRenderedImage rendered image}.
*
* @return The image as a Region Of Interest.
*
* @see #getRenderedImage
* @see #getPlanarImage
* @see #getRenderedOperation
*/
public ROI getImageAsROI() {
final ImageWorker worker = new ImageWorker(this);
worker.binarize(true);
return new ROI(worker.getRenderedImage());
}
/**
* Returns the <cite>Region Of Interest</cite> currently set, or {@code null} if none.
* The ROI applies to statistical methods like {@link #getMinimums} and {@link #getMaximums}.
* The default value is {@code null}, which means that statistics are computed on the whole
* image.
*
* @return The current Region Of Interest.
*
* @see #getMinimums
* @see #getMaximums
*/
public ROI getROI() {
return roi;
}
/**
* Sets the <cite>Region Of Interest</cite> (ROI). A {@code null} value sets the ROI to the
* whole {@linkplain #image}. The ROI applies to statistical methods like {@link #getMinimums}
* and {@link #getMaximums}.
*
* @param roi The new Region Of Interest.
*
* @see #getMinimums
* @see #getMaximums
*/
public void setROI(final ROI roi) {
this.roi = roi;
invalidateStatistics();
}
/**
* Returns the rendering hints for an image to be computed by subclasses.
* The default implementation returns the following hints:
* <p>
* <ul>
* <li>An {@linkplain ImageLayout image layout} with tiles size computed automatically
* from the current {@linkplain #image} size, unless an image layout was explicitly
* set as explained in the item below.</li>
* <li>Any additional hints specified through the {@link #setRenderingHint} method. If the
* user provided explicitly a {@link JAI#KEY_IMAGE_LAYOUT}, then the user layout has
* precedence over the automatic layout computed in the previous item.</li>
* </ul>
*
* @return The rendering hints to use for image computation (never {@code null}).
*/
public RenderingHints getRenderingHints() {
RenderingHints hints = ImageUtilities.getRenderingHints(image);
if (hints == null) {
hints = new RenderingHints(null);
if (commonHints != null) {
hints.add(commonHints);
}
} else if (commonHints != null) {
hints.add(commonHints);
}
if (Boolean.FALSE.equals(hints.get(ImageWorker.TILING_ALLOWED))) {
final ImageLayout layout = getImageLayout(hints);
if (commonHints == null || layout != commonHints.get(JAI.KEY_IMAGE_LAYOUT)) {
/*
* Setq the layout only if it is not a user-supplied object. We don't invoke
* modifiable(...) here because we don't want to write anything if the layout
* was specified by the user.
*/
layout.setTileWidth (image.getWidth());
layout.setTileHeight (image.getHeight());
layout.setTileGridXOffset(image.getMinX());
layout.setTileGridYOffset(image.getMinY());
hints.put(JAI.KEY_IMAGE_LAYOUT, layout);
}
}
if (tileCacheDisabled != 0) {
if (nullCache == null) {
nullCache = JAI.createTileCache(0);
}
hints.put(JAI.KEY_TILE_CACHE, nullCache);
}
return hints;
}
/**
* Returns the {@linkplain #getRenderingHints rendering hints}, but with a
* {@linkplain ComponentColorModel component color model} of the specified
* data type. The data type is changed only if no color model was explicitly
* specified by the user through {@link #getRenderingHints()}.
*
* @param type The data type (typically {@link DataBuffer#TYPE_BYTE}).
*/
final RenderingHints getRenderingHints(final int type) {
/*
* Gets the default hints, which usually contains only informations about tiling.
* If the user overridden the rendering hints with an explict color model, keep
* the user's choice.
*/
final RenderingHints hints = getRenderingHints();
ImageLayout layout = getImageLayout(hints);
if (layout.isValid(ImageLayout.COLOR_MODEL_MASK)) {
return hints;
}
final ColorModel oldCM = image.getColorModel();
setColorModel(hints, new ComponentColorModel(
oldCM.getColorSpace(),
oldCM.hasAlpha(), // If true, supports transparency.
oldCM.isAlphaPremultiplied(), // If true, alpha is premultiplied.
oldCM.getTransparency(), // What alpha values can be represented.
type)); // Type of primitive array used to represent pixel.
return hints;
}
/**
* Sets the {@link ColorModel} and a compatible {@link SampleModel} as an {@link ImageLayout}
* in the given hints. Note: this method canonicalize the given color model using
* {@code ColorModels.unique(cm)}, so there is no need to perform this step manually
* before to invoke this method.
*
* @param hints The hints where to store the modified {@link ImageLayout}.
* @param cm The color model to set in the given {@code hints}, or {@code null} to unset.
* @return The unique instance of then given color model.
*/
final ColorModel setColorModel(final RenderingHints hints, ColorModel cm) {
final ImageLayout layout = modifiable(getImageLayout(hints), hints);
if (cm != null) {
cm = ColorModels.unique(cm);
final SampleModel sm = cm.createCompatibleSampleModel(image.getWidth(), image.getHeight());
layout.setColorModel(cm);
layout.setSampleModel(sm);
} else {
layout.unsetValid(ImageLayout.COLOR_MODEL_MASK | ImageLayout.SAMPLE_MODEL_MASK);
}
return cm;
}
/**
* Gets the image layout from the specified rendering hints, creating a new one if needed.
* This method does not modify the specified hints. If the caller want to modify the image
* layout, then it must invoke {@link #modifiable} before doing so.
*/
static ImageLayout getImageLayout(final RenderingHints hints) {
final Object candidate = hints.get(JAI.KEY_IMAGE_LAYOUT);
if (candidate instanceof ImageLayout) {
return (ImageLayout) candidate;
}
return new ImageLayout();
}
/**
* Must be invoked after {@link #getImageLayout} if the caller intend to modify the layout.
*
* @param layout The layout returned by {@code getImageLayout}.
* @param hints The rendering hints that were specified to {@code getImageLayout}.
* @return The new image layout to use. May be a clone of the layout given in argument.
*/
final ImageLayout modifiable(ImageLayout layout, final RenderingHints hints) {
if (commonHints != null && layout == commonHints.get(JAI.KEY_IMAGE_LAYOUT)) {
layout = (ImageLayout) layout.clone();
}
// Following put is inconditional because the layout may have
// been created by new ImageLayout() in the above method.
hints.put(JAI.KEY_IMAGE_LAYOUT, layout);
return layout;
}
/**
* Returns the rendering hint for the specified key, or {@code null} if none.
* Newly created image worker have no rendering hints.
*
* @param key The key for which to get the rendering hint.
* @return The rendering hint for the given key.
*/
public Object getRenderingHint(final RenderingHints.Key key) {
ensureNonNull("key", key);
return (commonHints != null) ? commonHints.get(key) : null;
}
/**
* Sets a rendering hint to use for all images to be computed by subclasses.
* This method applies only to the next images to be computed. Images already
* computed before this method call (if any) will not be affected.
* <p>
* Some common examples:
* <p>
* <ul>
* <li><code>setRenderingHint({@linkplain JAI#KEY_TILE_CACHE}, null)</code>
* disables completely the tile cache.</li>
* <li><code>setRenderingHint({@linkplain ImageWorker#TILING_ALLOWED}, Boolean.FALSE)</code>
* forces all operators to produce untiled images.</li>
* </ul>
*
* @param key The key for which to set a rendering hint.
* @param value The value to assign to the given key.
*/
public void setRenderingHint(final RenderingHints.Key key, final Object value) {
ensureNonNull("key", key);
if (commonHints == null) {
commonHints = new RenderingHints(null);
}
commonHints.add(new RenderingHints(key, value));
}
/**
* Removes a rendering hint. Note that invoking this method is <strong>not</strong> the same
* than invoking {@link #setRenderingHint setRenderingHint(key, null)}. This is especially
* true for the {@linkplain javax.media.jai.TileCache tile cache} hint:
* <p>
* <ul>
* <li>{@code setRenderingHint(JAI.KEY_TILE_CACHE, null)} disables the use of any tile cache.
* In other words, this method call do request a tile cache, which happen to be the "null"
* cache.</li>
*
* <li>{@code removeRenderingHint(JAI.KEY_TILE_CACHE)} unsets any tile cache specified by a
* previous rendering hint. All images to be computed after this method call will save
* their tiles in the {@linkplain JAI#getTileCache JAI default tile cache}.</li>
* </ul>
*
* @param key The key for which to remove the rendering hint.
*/
public void removeRenderingHint(final RenderingHints.Key key) {
ensureNonNull("key", key);
if (commonHints != null) {
commonHints.remove(key);
}
}
/**
* Returns the number of bands in the {@linkplain #image}.
*
* @return The number of bands in the image.
*
* @see ImageWorker#retainBands
* @see SampleModel#getNumBands
*/
public int getNumBands() {
return image.getSampleModel().getNumBands();
}
/**
* Gets a property from the properties of the {@linkplain #image}. If the property name
* is not recognized, then {@link Image#UndefinedProperty} will be returned. This method
* does <strong>not</strong> inherits properties from the image specified at
* {@linkplain #ImageWorker(RenderedImage) construction time} - only properties generated
* by this class are returned.
*/
private Object getComputedProperty(final String name) {
final Object value = image.getProperty(name);
return (value == inheritanceStopPoint.getProperty(name)) ? Image.UndefinedProperty : value;
}
/**
* Returns the minimums and maximums values found in the image. Those extremas are
* returned as an array of the form {@code double[2][#bands]}.
*/
final double[][] getExtremas() {
Object extrema = getComputedProperty(EXTREMA);
if (!(extrema instanceof double[][])) {
final Integer ONE = 1;
image = ExtremaDescriptor.create(
image, // The source image.
roi, // The region of the image to scan. Default to all.
ONE, // The horizontal sampling rate. Default to 1.
ONE, // The vertical sampling rate. Default to 1.
null, // Whether to store extrema locations. Default to false.
ONE, // Maximum number of run length codes to store. Default to 1.
getRenderingHints());
extrema = getComputedProperty(EXTREMA);
}
return (double[][]) extrema;
}
/**
* Tells this builder that all statistics on pixel values (e.g. the "extrema" property
* in the {@linkplain #image}) should not be inherited from the source images (if any).
* This method should be invoked every time an operation changed the pixel values.
*/
final void invalidateStatistics() {
inheritanceStopPoint = image;
}
/**
* Returns the minimal values found in every {@linkplain #image} bands. If a
* {@linkplain #getROI Region Of Interest} is defined, then the statistics
* will be computed only over that region.
*
* @return The minimal values found in all bands. This is a direct reference to the
* array stored in {@linkplain RenderedImage#getProperty image properties},
* not a clone.
*
* @see #getMaximums
* @see #setROI
*/
public double[] getMinimums() {
return getExtremas()[0];
}
/**
* Returns the maximal values found in every {@linkplain #image} bands. If a
* {@linkplain #getROI Region Of Interest} is defined, then the statistics
* will be computed only over that region.
*
* @return The maximal values found in all bands. This is a direct reference to the
* array stored in {@linkplain RenderedImage#getProperty image properties},
* not a clone.
*
* @see #getMinimums
* @see #setROI
*/
public double[] getMaximums() {
return getExtremas()[1];
}
/**
* Returns the transparent pixel value, or -1 if none.
*
* @return The transparent pixel value, or -1 if none.
*/
public int getTransparentPixel() {
final ColorModel cm = image.getColorModel();
return (cm instanceof IndexColorModel) ? ((IndexColorModel) cm).getTransparentPixel() : -1;
}
/**
* Returns {@code true} if the {@linkplain #image} is
* {@linkplain Transparency#TRANSLUCENT translucent}.
*
* @return {@code true} if the image is translucent.
*
* @see ImageWorker#forceBitmaskIndexColorModel
*/
public boolean isTranslucent() {
return image.getColorModel().getTransparency() == Transparency.TRANSLUCENT;
}
/**
* Returns {@code true} if the {@linkplain #image} is tiled.
*
* @return {@code true} if the {@linkplain #image} is tiled.
*
* @see ImageWorker#tile
*
* @since 3.00
*/
public boolean isTiled() {
return image.getNumXTiles() != 1 || image.getNumYTiles() != 1;
}
/**
* Returns {@code true} if the {@linkplain #image} stores its pixel values in bytes.
* If {@code true}, then each sample values use at most 8 bits. However they may use
* less than 8 bits. For example a binary image stores 8 pixels by byte.
*
* @return {@code true} if the image stores pixel values as bytes.
*
* @see ImageWorker#format
*/
public boolean isBytes() {
return image.getSampleModel().getDataType() == TYPE_BYTE;
}
/**
* Returns {@code true} if the {@linkplain #image} is binary. An image is binary if it has
* only one band and uses only one bit per pixel. Such image can contains only two values:
* 0 and 1.
*
* @return {@code true} if the image is binary.
*
* @see ImageWorker#binarize(boolean)
*/
public boolean isBinary() {
return ImageUtil.isBinary(image.getSampleModel());
}
/**
* Returns {@code true} if the {@linkplain #image} uses an
* {@linkplain IndexColorModel Index Color Model}.
*
* @return {@code true} if the image is indexed.
*
* @see ImageWorker#setColorModelType
*/
public boolean isIndexed() {
return image.getColorModel() instanceof IndexColorModel;
}
/**
* Returns {@code true} if the {@linkplain #image} uses gray scale. This method returns
* {@code true} if one of the following conditions is true:
* <p>
* <ul>
* <li>The {@linkplain ColorSpace Color Space} is of
* {@linkplain ColorSpace#TYPE_GRAY type gray}</li>
* <li>The Color Model is an instance of {@link IndexColorModel} and the color map contains
* identical values for the Red, Green and Blue components. The color of fully transparent
* pixels is ignored by default, but this can be changed by assigning the value {@code FALSE}
* to the {@link #IGNORE_FULLY_TRANSPARENT_PIXELS} rendering hint.</li>
* </ul>
*
*
* {@section Relationship with Color Space type}
*
* If this method returns {@code false}, then it is guaranteed that {@link #getColorSpaceType}
* will not return {@link PaletteInterpretation#GRAY}. However the converse is not necessarily
* true. See the <cite>Index Color Model</cite> section in {@code getColorSpaceType()}.
*
* @return {@code true} if the {@linkplain #image} uses gray scale.
*
* @see #IGNORE_FULLY_TRANSPARENT_PIXELS
*
* @since 3.00
*/
public boolean isGrayScale() {
final ColorModel cm = image.getColorModel();
if (cm != null) {
if (cm instanceof IndexColorModel) {
Boolean ignoreTransparents = null;
if (commonHints != null) {
ignoreTransparents = (Boolean) commonHints.get(IGNORE_FULLY_TRANSPARENT_PIXELS);
}
if (ignoreTransparents == null) {
ignoreTransparents = Boolean.TRUE;
}
if (ColorUtilities.isGrayPalette((IndexColorModel) cm, ignoreTransparents)) {
return true;
}
}
final ColorSpace cs = cm.getColorSpace();
if (cs != null) {
return cs.getType() == ColorSpace.TYPE_GRAY;
}
}
return false;
}
/**
* Returns the type of the {@linkplain ColorSpace Color Space} used by the {@linkplain #image}.
* If the Color Space is known to this method, then it returns one of the constants defined
* in the {@link PaletteInterpretation} code list, or the {@link #IHS} constant. Otherwise
* this method returns {@code null}.
*
*
* {@section Index Color Model}
*
* RGB Color Space doesn't mean that pixel values are directly stored as RGB components.
* The main canvat is {@link IndexColorModel}, which has RGB color space despite the fact
* that images using such Color Model have only one band. In addition the color map in an
* {@code IndexColorModel} may contains only gray colors - this method will <strong>not</strong>
* returns the {@link PaletteInterpretation#GRAY} in this case since the Color Space still
* of {@linkplain ColorSpace#TYPE_RGB type RGB} in the Java2D sense. For detecting such
* {@code IndexColorModel} having only gray colors, use {@link #isGrayScale} instead.
*
* @return The palette interpretation inferred from the Color Space of the current image,
* or {@code null} if unknown.
*
* @see ImageWorker#setColorSpaceType
*
* @since 3.00
*/
public PaletteInterpretation getColorSpaceType() {
final ColorModel cm = image.getColorModel();
if (cm != null) {
final ColorSpace cs = cm.getColorSpace();
if (cs != null) {
switch (cs.getType()) {
case ColorSpace.TYPE_GRAY: return PaletteInterpretation.GRAY;
case ColorSpace.TYPE_RGB: return PaletteInterpretation.RGB;
case ColorSpace.TYPE_CMYK: return PaletteInterpretation.CMYK;
case ColorSpace.TYPE_HLS: return PaletteInterpretation.HLS;
case ColorSpace.TYPE_HSV: {
if (cs instanceof IHSColorSpace) {
return IHS;
}
}
}
}
}
return null;
}
/**
* Shows the current {@linkplain #image} in a window together with the operation chain as a
* {@linkplain javax.swing.JTree tree}. This method is provided mostly for debugging purpose.
* This method requires the {@code geotk-widgets.jar} file in the classpath.
*
* @throws HeadlessException if {@code geotk-widgets.jar} is not on the classpath, or
* if AWT can't create the window components.
*
* @see org.geotoolkit.gui.swing.image.OperationTreeBrowser#show(RenderedImage)
*/
@Debug
public void show() throws HeadlessException {
/*
* Uses reflection because the "gt2-widgets-swing.jar" dependency is optional and may not
* be available in the classpath. All the complicated stuff below is simply doing this call:
*
* OperationTreeBrowser.show(image);
*
* Tip: The @see tag in the above javadoc can be used as a check for the existence
* of class and method referenced below. Check for the javadoc warnings.
*/
final Class<?> c;
try {
c = Class.forName("org.geotoolkit.gui.swing.image.OperationTreeBrowser");
} catch (ClassNotFoundException cause) {
final HeadlessException e;
e = new HeadlessException("The \"geotk-widgets-swing.jar\" file is required.");
e.initCause(cause);
throw e;
}
try {
c.getMethod("show", new Class<?>[] {RenderedImage.class}).invoke(null, new Object[] {image});
} catch (InvocationTargetException e) {
final Throwable cause = e.getCause();
if (cause instanceof RuntimeException) {
throw (RuntimeException) cause;
}
if (cause instanceof Error) {
throw (Error) cause;
}
throw new AssertionError(e);
} catch (Exception e) {
/*
* ClassNotFoundException may be expected, but all other kinds of
* checked exceptions (and they are numerous...) are errors.
*/
throw new AssertionError(e);
}
}
/**
* If {@code false}, disables the tile cache. Invoking this method with value {@code true}
* cancel the last invocation with value {@code false}. If this method has been invoked many
* time with value {@code false}, then this method must be invoked the same amount of time
* with the value {@code true} for reenabling the cache.
*
* {@note This method name doesn't contain the usual <code>set</code> prefix because it
* doesn't really set a flag. Instead it increments or decrements a counter.}
*
* @param status {@code true} for enabling the tile cache, or {@code false} for disabling it.
*/
final void enableTileCache(final boolean status) {
if (status) {
if (tileCacheDisabled != 0) {
tileCacheDisabled--;
} else {
throw new IllegalStateException();
}
} else {
tileCacheDisabled++;
}
}
}