From f117c5227cb1be2d50ab33566b031e3347d4a788 Mon Sep 17 00:00:00 2001 From: dcollins Date: Tue, 6 Dec 2011 01:59:36 +0000 Subject: [PATCH] Added the TextureAtlas and TextureAtlasElement utility classes to package gov.nasa.worldwind.util. --- WorldWind/.idea/dictionaries/dcollins.xml | 2 + .../worldwind/util/MessageStrings.properties | 9 + .../gov/nasa/worldwind/util/TextureAtlas.java | 1247 +++++++++++++++++ .../worldwind/util/TextureAtlasElement.java | 472 +++++++ 4 files changed, 1730 insertions(+) create mode 100644 WorldWind/src/gov/nasa/worldwind/util/TextureAtlas.java create mode 100644 WorldWind/src/gov/nasa/worldwind/util/TextureAtlasElement.java diff --git a/WorldWind/.idea/dictionaries/dcollins.xml b/WorldWind/.idea/dictionaries/dcollins.xml index 67dbd202d..818abea89 100644 --- a/WorldWind/.idea/dictionaries/dcollins.xml +++ b/WorldWind/.idea/dictionaries/dcollins.xml @@ -1,6 +1,7 @@ + abgr args collada dcollins @@ -10,6 +11,7 @@ jburkey metoc namespaces + nvidia reinitializes renderable resize diff --git a/WorldWind/src/gov/nasa/worldwind/util/MessageStrings.properties b/WorldWind/src/gov/nasa/worldwind/util/MessageStrings.properties index 9d0253580..bcdfa6075 100644 --- a/WorldWind/src/gov/nasa/worldwind/util/MessageStrings.properties +++ b/WorldWind/src/gov/nasa/worldwind/util/MessageStrings.properties @@ -321,6 +321,7 @@ nullValue.ArrayIsNull=Array is null nullValue.AreaIsNull=Area is null nullValue.AreaUnit=Area unit is null nullValue.AssignmentIsNull=Assignment is null +nullValue.AtlasIsNull=Atlas is null nullValue.AttributeKeyIsNull=Attribute key is null nullValue.AttributesIsNull=Attributes reference is null nullValue.AVListIsNull=AVList is null @@ -1181,6 +1182,14 @@ TABReader.MissingRasterData=Missing TAB raster data {0} TABReader.NotEnoughControlPoints=Not enough TAB raster control points {0} TABReader.MissingOrInvalidFileName=TAB raster file name is missing or invalid {0} +TextureAtlas.AtlasIsFull=The image for \"{0}\" cannot be added because the texture atlas is full +TextureAtlas.InitialWidthInvalid=Invalid texture atlas initial width: {0} +TextureAtlas.InitialHeightInvalid=Invalid texture atlas initial height: {0} +TextureAtlas.ImageTooLarge=The image for \"{0}\" cannot be added because its dimensions exceed the texture atlas' maximum dimensions +TextureAtlas.MaxWidthInvalid=Invalid texture atlas max width: {0} +TextureAtlas.MaxHeightInvalid=Invalid texture atlas max height: {0} +TextureAtlas.ExceptionAddingImage=Exception adding texture atlas image: {0} + ThreadedTaskService.CancellingDuplicateTask=Cancelling duplicate task of {0} ThreadedTaskService.UncaughtExceptionDuringTask=Uncaught exception during task on thread {0} ThreadedTaskService.ResourceRejected=Task service rejected resource {0} diff --git a/WorldWind/src/gov/nasa/worldwind/util/TextureAtlas.java b/WorldWind/src/gov/nasa/worldwind/util/TextureAtlas.java new file mode 100644 index 000000000..b29a76cdd --- /dev/null +++ b/WorldWind/src/gov/nasa/worldwind/util/TextureAtlas.java @@ -0,0 +1,1247 @@ +/* + * Copyright (C) 2011 United States Government as represented by the Administrator of the + * National Aeronautics and Space Administration. + * All Rights Reserved. + */ +package gov.nasa.worldwind.util; + +import com.sun.opengl.impl.packrect.*; +import com.sun.opengl.util.texture.*; +import gov.nasa.worldwind.exception.WWRuntimeException; +import gov.nasa.worldwind.render.DrawContext; + +import javax.media.opengl.GL; +import java.awt.*; +import java.awt.image.*; +import java.util.*; +import java.util.Queue; + +/** + * Represents a texture composed of multiple independent images. The independent images are referred to as + * elements, and are packed into non-overlapping sub-rectangles within the texture atlas. The following NVIDIA + * document describes this technique: Improve Batching + * Using Texture Atlases + * + * @author dcollins + * @version $Id$ + */ +public class TextureAtlas +{ + /** + * Implementation of the JOGL BackingStoreManager interface for texture atlas. This is used by the JOGL {@link + * RectanglePacker}, and delegates calls from a JOGL rectangle packer to methods in this texture atlas. + */ + protected class AtlasBackingStore implements BackingStoreManager + { + /** + * {@inheritDoc} + *

+ * Calls {@link TextureAtlas#createBackingImage(int, int)} with the specified width and height. + */ + public Object allocateBackingStore(int w, int h) + { + return createBackingImage(w, h); + } + + /** + * {@inheritDoc} + *

+ * Calls {@link TextureAtlas#disposeBackingImage()}. + */ + public void deleteBackingStore(Object backingStore) + { + disposeBackingImage(); + } + + /** + * {@inheritDoc} + *

+ * Returns false, indicating that the rectangle packer should just expand. When configured to do + * so, texture atlas evicts old elements in additionFailed, when this texture atlas is full and the + * addition would otherwise fail. + */ + public boolean preExpand(Rect cause, int attemptNumber) + { + return false; + } + + /** + * {@inheritDoc} + *

+ * If this texture atlas is configured to evicts old elements, this attempts to remove the oldest one then + * exits, allowing the caller to attempt the addition again. This throws a WWRuntimeException if this texture + * atlas is not configured to evict old elements, or if there are no more elements to evict. + * + * @throws WWRuntimeException if this backing store cannot fit the rectangle in its layout. + */ + public void additionFailed(Rect cause, int attemptNumber) + { + if (!isEvictOldElements() || !removeLeastRecentlyUsedEntry()) + throw new WWRuntimeException(Logging.getMessage("TextureAtlas.AtlasIsFull")); + } + + /** + * {@inheritDoc} + *

+ * Calls {@link TextureAtlas#beginMoveEntries(java.awt.image.BufferedImage, java.awt.image.BufferedImage)}, + * casting the specified backing stores to BufferedImages. + */ + public void beginMovement(Object oldBackingStore, Object newBackingStore) + { + beginMoveEntries((BufferedImage) oldBackingStore, (BufferedImage) newBackingStore); + } + + /** + * {@inheritDoc} + *

+ * Calls {@link TextureAtlas#moveEntry(java.awt.image.BufferedImage, com.sun.opengl.impl.packrect.Rect, + * java.awt.image.BufferedImage, com.sun.opengl.impl.packrect.Rect)}, casting the specified backing stores to + * BufferedImages. + */ + public void move(Object oldBackingStore, Rect oldLocation, Object newBackingStore, Rect newLocation) + { + moveEntry((BufferedImage) oldBackingStore, oldLocation, (BufferedImage) newBackingStore, newLocation); + } + + /** + * {@inheritDoc} + *

+ * Calls {@link TextureAtlas#endMoveEntries(java.awt.image.BufferedImage, java.awt.image.BufferedImage)}, + * casting the specified backing stores to BufferedImages. + */ + public void endMovement(Object oldBackingStore, Object newBackingStore) + { + endMoveEntries((BufferedImage) oldBackingStore, (BufferedImage) newBackingStore); + } + } + + /** + * Represents an image element in a texture atlas. Each entry indicates the element's key, the image's rectangle in + * the backing image, the actual image's offset within that rectangle, the image's actual width and height, and a + * timestamp indicating the last time the element was used. Implements the {@link Comparable} interface by comparing + * the lastUsed timestamp, ordered from least recently used to most recently used. + */ + protected static class Entry implements Comparable + { + /** Indicates the element's key. Initialized during construction. */ + public final Object key; + /** Indicates the element's bounding rectangle within the texture atlas. Initialized during construction. */ + public Rect rect; + /** Indicates the element's image X offset withing the bounding rectangle. Initialized during construction. */ + public int imageOffsetX; + /** Indicates the element's image Y offset withing the bounding rectangle. Initialized during construction. */ + public int imageOffsetY; + /** + * Indicates the element's image width. May be smaller than the bounding rectangle's width. Initialized during + * construction. + */ + public int imageWidth; + /** + * Indicates the element's image height. May be smaller than the bounding rectangle's height. Initialized during + * construction. + */ + public int imageHeight; + /** Indicates the last time this entry was used. */ + public long lastUsed; + + /** + * Constructs a texture atlas entry corresponding with a texture atlas element with the specified key, bounding + * rectangle, and image offsets within the bounding rectangle. + * + * @param key the element's key. + * @param rect the element's bounding rectangle within the texture atlas. + * @param imageOffsetX the element's image X offset withing the bounding rectangle. + * @param imageOffsetY the element's image Y offset withing the bounding rectangle. + * @param imageWidth the element's image width. May be smaller than the bounding rectangle's width. + * @param imageHeight the element's image height. May be smaller than the bounding rectangle's height. + */ + public Entry(Object key, Rect rect, int imageOffsetX, int imageOffsetY, int imageWidth, int imageHeight) + { + this.key = key; + this.rect = rect; + this.imageOffsetX = imageOffsetX; + this.imageOffsetY = imageOffsetY; + this.imageWidth = imageWidth; + this.imageHeight = imageHeight; + } + + /** + * Compares this texture atlas entry's last used timestamp to that of the specified entry. This returns -1 if + * this entry's last used time is earlier than the specified entry's, 0 if the two entries have the same last + * used time, and 1 if this entry's last used time is later than the specified entry's. + * + * @param that the texture atlas entry this entry is compared to. + * + * @return -1, 0, or 1 if this entry's last used time is earlier than, the same as, or later than the specified + * entry's last used time. + */ + public int compareTo(Entry that) + { + if (that == null) + { + String msg = Logging.getMessage("nullValue.EntryIsNull"); + Logging.logger().severe(msg); + throw new IllegalArgumentException(msg); + } + + return this.lastUsed < that.lastUsed ? -1 : this.lastUsed == that.lastUsed ? 0 : 1; + } + } + + /** The texture atlas' default setting for the useMipMaps property: true. */ + protected static final boolean DEFAULT_USE_MIP_MAPS = true; + /** The texture atlas' default setting for the useAnisotropy property: true. */ + protected static final boolean DEFAULT_USE_ANISOTROPY = true; + /** The texture atlas' default maximum vertical fragmentation: 0.7. */ + protected static final double DEFAULT_MAX_VERTICAL_FRAGMENTATION = 0.7; + + /** + * Indicates this texture atlas' maximum total width, in pixels. This is specified during construction and is used + * to determine when an image is too large to fit in this texture atlas. + */ + protected int maxWidth; + /** + * Indicates this texture atlas' maximum total height, in pixels. This is specified during construction and is used + * to determine when an image is too large to fit in this texture atlas. + */ + protected int maxHeight; + /** + * Indicates whether this texture atlas generates mip-maps for each atlas element. true if this texture + * atlas generates mip-maps, and false otherwise. Specified during construction. + */ + protected boolean useMipMaps; + /** + * Indicates whether this texture atlas applies an anisotropic filter to each atlas element. true if + * this texture atlas applies an anisotropic filter, and false otherwise. This has no effect if + * useMipMaps is false. Specified during construction. + */ + protected boolean useAnisotropy; + /** + * Indicates whether this texture atlas evicts old elements in order to make room for a new element when the atlas + * is full. true if this atlas evicts elements to make room for new elements, and false + * otherwise. Initially code false. + */ + protected boolean evictOldElements; + /** + * Indicates the maximum amount of vertical fragmentation this texture atlas allows before compacting its elements. + * Initialized to DEFAULT_MAX_VERTICAL_FRAGMENTATION. + */ + protected double maxVerticalFragmentation = DEFAULT_MAX_VERTICAL_FRAGMENTATION; + /** + * The JOGL rectangle packer used by this texture atlas to determine how to pack the elements within this texture + * atlas' backing image. Initialized during construction. + */ + protected RectanglePacker rectPacker; + /** + * Maps element keys to their corresponding entry. This enables the texture atlas to access the information about + * each element in constant time using its key. Initialized to a new HashMap. + */ + protected Map entryMap = new HashMap(); + /** + * Indicates the rectangle within this texture atlas' backing image that is currently out-of-sync with its + * corresponding OpenGL texture. The dirty rectangle is null when this texture atlas' backing images is + * synchronized with its OpenGL texture. Initially null. + */ + protected Rectangle dirtyRect; + /** + * Indicates the color used to fill regions of this texture atlas that do not contain a sub-image element. Initially + * transparent black (R=0, G=0, B=0, A=0). + */ + protected Color clearColor = new Color(0, 0, 0, 0); + /** + * Temporary AWT graphics instance used to move image elements during a beginMovement/endMovement block. This + * property is assigned in beginMovement, used in move, then cleared in endMovement. Initially null. + */ + protected Graphics2D g; + /** + * Indicates the current key corresponding to this texture atlas' OpenGL texture in the GPU resource cache. This key + * is assigned to a new instance whenever this texture atlas creates new backing image. Initialized to a new + * Object. + */ + protected Object textureKey = new Object(); + /** + * Queue of texture keys corresponding to disposed backing images. These keys are disposed during the next call to + * bind. While disposed backing textures would eventually be evicted by the GPU resource cache, + * explicitly disposing them avoids polluting the GPU resource cache with orphaned textures that correspond only to + * this texture atlas. + */ + protected Queue disposedTextureKeys = new ArrayDeque(); + + /** + * Constructs a texture atlas with the specified initial and maximum dimensions. The dimensions must be greater than + * zero, and the maximum dimensions must be greater than or equal to the initial dimensions. The constructed texture + * atlas generates mip-maps and applies an anisotropic filter to each element. + * + * @param initialWidth the texture atlas' initial width, in pixels. Must be greater than zero. + * @param initialHeight the texture atlas' initial height, in pixels. Must be greater than zero. + * @param maxWidth the texture atlas' maximum width, in pixels. Must be greater than or equal to initialWidth. + * @param maxHeight the texture atlas' maximum height, in pixels. Must be greater than or equal to + * initialHeight. + * + * @throws IllegalArgumentException if any of initialWidth, initialHeight, maxWidth, or maxHeight are less than or + * equal to zero, if maxWidth is less than initialWidth, or if maxHeight is less + * than initialHeight. + */ + public TextureAtlas(int initialWidth, int initialHeight, int maxWidth, int maxHeight) + { + this(initialWidth, initialHeight, maxWidth, maxHeight, DEFAULT_USE_MIP_MAPS, DEFAULT_USE_ANISOTROPY); + } + + /** + * Constructs a texture atlas with the specified initial and maximum dimensions. The dimensions must be greater than + * zero, and the maximum dimensions must be greater than or equal to the initial dimensions. This constructor + * enables specification of whether the texture atlas generates mip-maps and applies an anisotropic filter to each + * element. + * + * @param initialWidth the texture atlas' initial width, in pixels. Must be greater than zero. + * @param initialHeight the texture atlas' initial height, in pixels. Must be greater than zero. + * @param maxWidth the texture atlas' maximum width, in pixels. Must be greater than or equal to initialWidth. + * @param maxHeight the texture atlas' maximum height, in pixels. Must be greater than or equal to + * initialHeight. + * @param useMipMaps whether to generate mip-maps for each atlas element. true to generate mip-maps, + * and false otherwise. + * @param useAnisotropy whether to apply an anisotropic filter to each atlas element. true to apply an + * anisotropic filter, and false otherwise. This has no effect if useMipMaps is + * false. + * + * @throws IllegalArgumentException if any of initialWidth, initialHeight, maxWidth, or maxHeight are less than or + * equal to zero, if maxWidth is less than initialWidth, or if maxHeight is less + * than initialHeight. + */ + public TextureAtlas(int initialWidth, int initialHeight, int maxWidth, int maxHeight, boolean useMipMaps, + boolean useAnisotropy) + { + if (initialWidth < 1) + { + String msg = Logging.getMessage("TextureAtlas.InitialWidthInvalid", initialWidth); + Logging.logger().severe(msg); + throw new IllegalArgumentException(msg); + } + + if (initialHeight < 1) + { + String msg = Logging.getMessage("TextureAtlas.InitialHeightInvalid", initialHeight); + Logging.logger().severe(msg); + throw new IllegalArgumentException(msg); + } + + if (maxWidth < initialWidth) + { + String msg = Logging.getMessage("TextureAtlas.MaxWidthInvalid", maxWidth); + Logging.logger().severe(msg); + throw new IllegalArgumentException(msg); + } + + if (maxHeight < initialHeight) + { + String msg = Logging.getMessage("TextureAtlas.MaxWidthInvalid", maxHeight); + Logging.logger().severe(msg); + throw new IllegalArgumentException(msg); + } + + // Create a JOGL rectangle packer with the specified initial and maximum dimensions. The rectangle packer + // determines the placement of each image within this texture atlas, and determines when to expand the atlas. + this.rectPacker = this.createRectanglePacker(initialWidth, initialHeight); + this.rectPacker.setMaxSize(maxWidth, maxHeight); + + this.maxWidth = maxWidth; + this.maxHeight = maxHeight; + this.useMipMaps = useMipMaps; + this.useAnisotropy = useAnisotropy; + } + + /** + * Returns a new JOGL rectangle packer that is used by this texture atlas to determine how to pack the elements + * within this texture atlas' backing image. By default, this returns a rectangle packer with an {@link + * AtlasBackingStore} as the backing store manager. Called during construction. + * + * @param initialWidth this texture atlas' initial width. + * @param initialHeight this texture atlas' initial height. + * + * @return a new JOGL rectangle packer with the specified initial dimensions. + */ + protected RectanglePacker createRectanglePacker(int initialWidth, int initialHeight) + { + return new RectanglePacker(new AtlasBackingStore(), initialWidth, initialHeight); + } + + /** + * Indicates this texture atlas' current width, in pixels. + * + * @return this texture atlas' current width. + */ + public int getWidth() + { + return ((BufferedImage) this.rectPacker.getBackingStore()).getWidth(); + } + + /** + * Indicates this texture atlas' current height, in pixels. + * + * @return this texture atlas' current height. + */ + public int getHeight() + { + return ((BufferedImage) this.rectPacker.getBackingStore()).getHeight(); + } + + /** + * Indicates this texture atlas' maximum width, in pixels. + * + * @return this texture atlas' maximum width. + */ + public int getMaxWidth() + { + return this.maxWidth; + } + + /** + * Indicates this texture atlas' maximum height, in pixels. + * + * @return this texture atlas' maximum height. + */ + public int getMaxHeight() + { + return this.maxHeight; + } + + /** + * Indicates whether this texture atlas generates mip-maps for each atlas element. Specified during construction. + * + * @return true if this texture atlas generates mip-maps, and false otherwise. + */ + public boolean isUseMipMaps() + { + return this.useMipMaps; + } + + /** + * Indicates whether this texture atlas applies an anisotropic filter to each atlas element. This has no effect if + * useMipMaps is false. Specified during construction. + * + * @return true if this texture atlas applies an anisotropic filter, and false otherwise. + */ + public boolean isUseAnisotropy() + { + return this.useAnisotropy; + } + + /** + * Indicates whether this texture atlas evicts the oldest elements in order to make room for a new element when the + * atlas is full. + * + * @return true if this atlas evicts old elements to make room for new elements, and false + * otherwise. + * + * @see #setEvictOldElements(boolean) + */ + public boolean isEvictOldElements() + { + return this.evictOldElements; + } + + /** + * Specifies whether this texture atlas should evict the oldest elements in order to make room for a new element + * when the atlas is full. When disabled, calling add with an element that does not fit in the current + * atlas layout causes this texture atlas to throw an exception if the atlas cannot be expanded. When enabled, the + * oldest elements are evicted until there is enough space to fit the element in the layout. + * + * @param evictOldElements true if this atlas should evict old elements to make room for new elements, + * and false otherwise. + */ + public void setEvictOldElements(boolean evictOldElements) + { + this.evictOldElements = evictOldElements; + } + + /** + * Returns the number of elements currently in this texture atlas. + * + * @return the number of elements in this texture atlas, or 0 if this atlas does not contain any elements. + */ + public int getNumElements() + { + return this.entryMap.size(); + } + + /** + * Indicates whether this texture atlas contains any elements. + * + * @return true if this texture atlas contains at least one element, and false otherwise. + */ + public boolean isEmpty() + { + return this.entryMap.isEmpty(); + } + + /** + * Adds a new element to this texture atlas with the specified key and image. The image's dimensions must be less + * than or equal to this texture atlas' maximum dimensions. If this texture atlas is not configured to evict old + * entries, this throws an exception if the image does not fit in the current atlas layout and the atlas cannot be + * expanded. + *

+ * This adds a one pixel border around the specified image in this texture atlas' backing image by copying the + * image's outer pixels into a border surrounding the original image. This border avoids sampling pixels from + * neighboring atlas elements when an OpenGL box filter is applied to this image. This means that the atlas actually + * requires space for an image with dimensions (width + 2, height + 2), where width and height are the image's + * original dimensions. + * + * @param key an object used to reference the image. + * @param image the image to add. + * + * @throws IllegalArgumentException if either the key or image is null, if the image dimensions are + * greater than this texture atlas' maximum dimensions. + * @throws WWRuntimeException if this texture atlas is too full to fit the image in its layout. + */ + public void add(Object key, BufferedImage image) + { + if (key == null) + { + String msg = Logging.getMessage("nullValue.KeyIsNull"); + Logging.logger().severe(msg); + throw new IllegalArgumentException(msg); + } + + if (image == null) + { + String msg = Logging.getMessage("nullValue.ImageIsNull"); + Logging.logger().severe(msg); + throw new IllegalArgumentException(msg); + } + + // Add two to account for the 1 pixel border we add to the image. + if (image.getWidth() + 2 > this.maxWidth || image.getHeight() + 2 > this.maxHeight) + { + String msg = Logging.getMessage("TextureAtlas.ImageTooLarge", key); + Logging.logger().severe(msg); + throw new IllegalArgumentException(msg); + } + + try + { + this.doAdd(key, image); + } + catch (Exception e) + { + // doAdd throws a WWRuntimeException when the rectangle packer cannot fit the specified image into the + // backing store. + String msg = Logging.getMessage("TextureAtlas.AtlasIsFull", key); + Logging.logger().severe(msg); + throw new WWRuntimeException(msg); + } + } + + /** + * Adds a new element to this texture atlas with the specified key and image. + * + * @param key an object used to reference the image. + * @param image the image to add. + * + * @throws WWRuntimeException if this texture atlas is too full to fit the image in its layout. + */ + protected void doAdd(Object key, BufferedImage image) + { + // Remove any existing entry and add it to the list of unused entries before attempting to add one with the same + // key. This ensures that the old entry is not orphaned in the rectangle packer's list of rectangles. + Entry entry = this.entryMap.remove(key); + if (entry != null) + { + this.doRemove(entry); + } + + // Create a rectangle for the element with enough additional space to provide a 1 pixel border around the image. + Rect rect = new Rect(0, 0, image.getWidth() + 2, image.getHeight() + 2, null); + + // Add an entry to the entryMap to provide constant time access to the entry's rectangle and attributes, and + // mark the entry as used at the current time. We offset the image by 1 pixel within its rectangle to provide a + // 1 pixel border around the image + entry = new Entry(key, rect, 1, 1, image.getWidth(), image.getHeight()); + this.markUsed(entry); + this.entryMap.put(key, entry); + + // Add the element's rectangle to the rectangle packer, expanding or rearranging the existing elements as needed + // to incorporate the new element. This call sets the new rectangle's x and y coordinates to the rectangle's + // location within the backing image. + this.rectPacker.add(rect); + + // Copy the image's pixels into the rectangle packer's backing image at point determined by the rectangle + // packer, replacing backing store pixels with those of the image. Note that the rectangle's x and y coordinates + // are assigned upon adding it to the rectangle packer above. We draw a 1 pixel border around the image in order + // to avoid sampling pixels from neighboring atlas elements when an OpenGL box filter is applied to this image. + int imageX = rect.x() + entry.imageOffsetX; + int imageY = rect.y() + entry.imageOffsetY; + this.drawImage((BufferedImage) this.rectPacker.getBackingStore(), image, imageX, imageY, true); + + // Mark the rectangle associated with this entry as dirty so the OpenGL texture is synchronized with the backing + // image upon the next call to bind. + this.markDirty(rect.x(), rect.y(), rect.w(), rect.h()); + } + + /** + * Removes the element with the specified key from this texture atlas, freeing the space it occupied to be used by + * other elements. If this texture atlas does not contain an element with the specified key, this returns + * false but otherwise does nothing. + * + * @param key an object used to reference the element to remove. + * + * @return true if this texture atlas contained an element with the specified key, and + * false otherwise. + * + * @throws IllegalArgumentException if the key is null. + */ + public boolean remove(Object key) + { + if (key == null) + { + String msg = Logging.getMessage("nullValue.KeyIsNull"); + Logging.logger().severe(msg); + throw new IllegalArgumentException(msg); + } + + Entry entry = this.entryMap.remove(key); + if (entry != null) + { + this.doRemove(entry); + } + + return entry != null; + } + + /** + * Removes the element corresponding to the specified entry from this texture atlas, freeing the space it occupied + * to be used by other elements. + * + * @param entry the entry to remove. + */ + protected void doRemove(Entry entry) + { + Rect rect = entry.rect; + + // Remove the element's rectangle from the JOGL rectangle packer. This frees space for the + this.rectPacker.remove(rect); + + // Fill the element's rectangle in the backing image with the clear color, then mark the rectangle as dirty + // so the OpenGL texture is synchronized with the backing image during the next call to bind. + this.clearRect((BufferedImage) this.rectPacker.getBackingStore(), rect.x(), rect.y(), rect.w(), rect.h()); + this.markDirty(rect.x(), rect.y(), rect.w(), rect.h()); + + // Compact the remaining entries if the vertical fragmentation ratio is larger than this texture atlas' + // configured threshold. This avoids wasting texture space when many elements of different sizes are + // subsequently added and removed. + if (this.rectPacker.verticalFragmentationRatio() > this.maxVerticalFragmentation) + this.rectPacker.compact(); + } + + /** + * Indicates whether this texture atlas contains an element with the specified key. + * + * @param key the key which the element is referenced by. + * + * @return true if this texture atlas contains an element with the specified key, and + * false otherwise. + * + * @throws IllegalArgumentException if the key is null. + */ + public boolean contains(Object key) + { + if (key == null) + { + String msg = Logging.getMessage("nullValue.KeyIsNull"); + Logging.logger().severe(msg); + throw new IllegalArgumentException(msg); + } + + return this.entryMap.containsKey(key); + } + + /** + * Returns the image dimensions associated with an element in this texture atlas. This returns null + * this texture atlas does not contain the element. + * + * @param key the key which the element is referenced by. + * + * @return the image dimensions corresponding to the specified element, or null if this texture atlas + * does not contain the element. + */ + public Dimension getSize(Object key) + { + if (key == null) + { + String msg = Logging.getMessage("nullValue.KeyIsNull"); + Logging.logger().severe(msg); + throw new IllegalArgumentException(msg); + } + + Entry entry = this.entryMap.get(key); + if (entry == null) + return null; + + // Mark that the entry has been used at the current time. + this.markUsed(entry); + + return new Dimension(entry.imageWidth, entry.imageHeight); + } + + /** + * Returns the OpenGL texture coordinates associated with an element in this texture atlas. This returns + * null this texture atlas does not contain the element. + * + * @param key the key which the element is referenced by. + * + * @return the OpenGL texture coordinates corresponding to the specified element, or null if this + * texture atlas does not contain the element. + */ + public TextureCoords getTexCoords(Object key) + { + if (key == null) + { + String msg = Logging.getMessage("nullValue.KeyIsNull"); + Logging.logger().severe(msg); + throw new IllegalArgumentException(msg); + } + + Entry entry = this.entryMap.get(key); + if (entry == null) + return null; + + // Mark that the entry has been used at the current time. + this.markUsed(entry); + + // Compute the lower-left and upper-right pixels corresponding to the element's image. We use the image width + // and height instead of the rectangle's width and height because the image may be smaller than its rectangle. + float x1 = entry.rect.x() + entry.imageOffsetX; + float y1 = entry.rect.y() + entry.imageOffsetY; + float x2 = x1 + entry.imageWidth; + float y2 = y1 + entry.imageHeight; + + // Compute the lower-left and upper-right OpenGL texture coordinates corresponding to the element's image. This + // step converts pixel locations in the range [0, width] or [0, height] to the range [0, 1]. + BufferedImage backingImage = (BufferedImage) this.rectPacker.getBackingStore(); + float tx1 = x1 / (float) backingImage.getWidth(); + float tx2 = x2 / (float) backingImage.getWidth(); + float ty1 = y1 / (float) backingImage.getHeight(); + float ty2 = y2 / (float) backingImage.getHeight(); + + // Note that we flip the element's y coordinates. The backing image uses AWT coordinates which places its origin + // in the upper-left corner, while the OpenGL texture expects the origin to be in the lower-left corner. + return new TextureCoords(tx1, ty2, tx2, ty1); + } + + /** Removes all elements from this texture atlas. The backing image retains its current dimensions after this call. */ + public void clear() + { + this.rectPacker.clear(); + this.entryMap.clear(); + + // We've removed all entries from this texture atlas, so mark the entire backing image as dirty so the OpenGL + // texture is synchronized with the backing image during the next call to bind. + BufferedImage backingImage = (BufferedImage) this.rectPacker.getBackingStore(); + this.markDirty(0, 0, backingImage.getWidth(), backingImage.getHeight()); + } + + /** + * Binds this texture atlas' OpenGL texture to the GLContext attached to the draw context. Before binding, this + * updates the OpenGL texture as necessary to reflect changes in this texture atlas since the last call to + * bind. + * + * @param dc the current draw context. + * + * @return true if the texture is bound, and false otherwise. + * + * @throws IllegalArgumentException if the draw context is null. + */ + public boolean bind(DrawContext dc) + { + if (dc == null) + { + String msg = Logging.getMessage("nullValue.DrawContextIsNull"); + Logging.logger().severe(msg); + throw new IllegalArgumentException(msg); + } + + // Remove textures corresponding to this texture atlas' disposed backing images from the draw context's GPU + // resource cache before synchronizing the OpenGL texture. We do this before synchronizing to ensure that this + // texture atlas does not cause unnecessary cache thrashing. + this.disposeOldTextures(dc); + + // Synchronize the OpenGL texture with the backing image, creating OpenGL texture as necessary. + Texture texture = this.syncTexture(dc); + if (texture != null) + { + texture.bind(); + return true; + } + else + { + return false; + } + } + + /** + * Creates and returns a BufferedImage with the specified dimensions to use as this texture atlas' backing image. + * The returned image must be at least as large as the specified dimensions, but may be larger. By default, this + * always returns an image with power-of-two dimensions in order to maximize compatibility with different graphics + * cards. + * + * @param width the backing image's minimum width, in pixels. + * @param height the backing image's minimum height, in pixels. + * + * @return a new BufferedImage to use as this texture atlas' backing image. + */ + protected BufferedImage createBackingImage(int width, int height) + { + // Create an image with power-of-two dimensions in order to maximize compatibility with different graphics + // cards. This eliminates the need for the individual images to have power-of-two dimensions. + int potWidth = WWMath.powerOfTwoCeiling(width); + int potHeight = WWMath.powerOfTwoCeiling(height); + + // Create a buffered image with the type 4BYTE_ABGR_PRE and fill it with the clear color. We use this image type + // in order to avoid creating an extra copy when the JOGL TextureIO utility creates a texture from this image's + // data. + BufferedImage bi = new BufferedImage(potWidth, potHeight, BufferedImage.TYPE_4BYTE_ABGR_PRE); + this.clearRect(bi, 0, 0, potWidth, potHeight); + + return bi; + } + + /** Disposes of this texture atlas' current backing image. */ + protected void disposeBackingImage() + { + // The rectangle packer is expanding or compacting the backing image, so we need to dispose of the current + // backing image and its associated texture. We dispose of the texture by generating a new texture key and + // adding the old key to the list of disposed texture keys. The current key may not be associated with any + // texture, in which case it is ignored when processing the disposed texture keys. We do nothing to dispose of + // the backing image, because this texture atlas does not retain any references to it. + Object oldKey = this.textureKey; + this.textureKey = new Object(); + this.disposedTextureKeys.add(oldKey); + } + + /** + * Fills the specified rectangle with the clear color in the backing image. + * + * @param backingImage the destination backing image to fill with the clear color. + * @param x the X coordinate of the rectangle's upper-left corner, in pixels. + * @param y the Y coordinates of the rectangle's upper-left corner, in pixels. + * @param width the rectangle's width, in pixels. + * @param height the rectangle's height, in pixels. + */ + protected void clearRect(BufferedImage backingImage, int x, int y, int width, int height) + { + Graphics2D g = backingImage.createGraphics(); + try + { + g.setComposite(AlphaComposite.Src); // Replace destination pixels with the clear color (disables blending). + g.setColor(this.clearColor); + g.fillRect(x, y, width, height); + } + finally + { + g.dispose(); + } + } + + /** + * Draws the specified image in the backing image at the specified (x, y) location. If drawBorder is + * true, this copies the image's outer pixels into 1 pixel border surrounding the original image. This + * border avoids sampling pixels from neighboring atlas elements when an OpenGL box filter is applied to this + * image. + * + * @param backingImage the destination backing image to draw into. + * @param image the source image to draw. + * @param x the X coordinate of the image's upper-left corner, in pixels. + * @param y the Y coordinates of the image's upper-left corner, in pixels. + * @param drawBorder true this copy the image's outer pixels into 1 pixel border surrounding the + * original image, or false to draw only the image. + */ + protected void drawImage(BufferedImage backingImage, BufferedImage image, int x, int y, boolean drawBorder) + { + int w = image.getWidth(); + int h = image.getHeight(); + + Graphics2D g = backingImage.createGraphics(); + try + { + // Replace destination pixels with source pixels (disables blending). + g.setComposite(AlphaComposite.Src); + + // Copy the entire image to (x, y). + g.drawImage(image, x, y, null); + + if (drawBorder) + { + // Copy the image's top left corner to (x - 1, y - 1). + g.drawImage(image, + x - 1, y - 1, x, y, // dstX1, dstY1, dstX2, dstY2 + 0, 0, 1, 1, // srcX1, srcY1, srcX2, srcY2 + null); + + // Copy the image's top row to (x, y - 1). + g.drawImage(image, + x, y - 1, x + w, y, // dstX1, dstY1, dstX2, dstY2 + 0, 0, w, 1, // srcX1, srcY1, srcX2, srcY2 + null); + + // Copy the image's top right corner to (x + w, y - 1). + g.drawImage(image, + x + w, y - 1, x + w + 1, y, // dstX1, dstY1, dstX2, dstY2 + w - 1, 0, w, 1, // srcX1, srcY1, srcX2, srcY2 + null); + + // Copy the image's right column to (x + w, y). + g.drawImage(image, + x + w, y, x + w + 1, y + h, // dstX1, dstY1, dstX2, dstY2 + w - 1, 0, w, h, // srcX1, srcY1, srcX2, srcY2 + null); + + // Copy the image's bottom right corner to (x + w, y + h). + g.drawImage(image, + x + w, y + h, x + w + 1, y + h + 1, // dstX1, dstY1, dstX2, dstY2 + w - 1, h - 1, w, h, // srcX1, srcY1, srcX2, srcY2 + null); + + // Copy the image's bottom row to (x, y + h). + g.drawImage(image, + x, y + h, x + w, y + h + 1, // dstX1, dstY1, dstX2, dstY2 + 0, h - 1, w, h, // srcX1, srcY1, srcX2, srcY2 + null); + + // Copy the image's bottom left corner to (x - 1, y + h). + g.drawImage(image, + x - 1, y + h, x, y + h + 1, // dstX1, dstY1, dstX2, dstY2 + 0, h - 1, 1, h, // srcX1, srcY1, srcX2, srcY2 + null); + + // Copy the image's left column to (x - 1, y). + g.drawImage(image, + x - 1, y, x, y + h, // dstX1, dstY1, dstX2, dstY2 + 0, 0, 1, h, // srcX1, srcY1, srcX2, srcY2 + null); + } + } + finally + { + g.dispose(); + } + } + + /** + * Called when the atlas is performing a full re-layout of its elements, just before the layout begins. If this + * texture atlas' dimensions are changing, the specified backing images refer to separate instances with different + * dimensions. If this texture atlas is performing a re-layout in place, the specified backing images refer to the + * same instance. + * + * @param oldBackingImage the backing image corresponding to the previous layout. + * @param newBackingImage the backing image corresponding to the new layout. + */ + @SuppressWarnings( {"UnusedParameters"}) + protected void beginMoveEntries(BufferedImage oldBackingImage, BufferedImage newBackingImage) + { + if (this.g != null) // This should never happen, but we check anyway. + this.g.dispose(); + + this.g = newBackingImage.createGraphics(); + this.g.setComposite(AlphaComposite.Src); // Replace destination pixels with source pixels. + } + + /** + * Called when the atlas is performing a full re-layout of its elements, just after the layout ends. If this texture + * atlas' dimensions have changed, the specified backing images refer to separate instances with different + * dimensions. If this texture atlas has performed a re-layout in place, the specified backing images refer to the + * same instance. + * + * @param oldBackingImage the backing image corresponding to the previous layout. + * @param newBackingImage the backing image corresponding to the new layout. + */ + @SuppressWarnings( {"UnusedParameters"}) + protected void endMoveEntries(BufferedImage oldBackingImage, BufferedImage newBackingImage) + { + if (this.g != null) // This should never happen, but we check anyway. + { + this.g.dispose(); + this.g = null; + } + + // We've removed all entries from this texture atlas, so mark the entire backing image as dirty so it's + // synchronized with the OpenGL texture during the next call to bind. + this.markDirty(0, 0, newBackingImage.getWidth(), newBackingImage.getHeight()); + } + + /** + * Called for each atlas element when the atlas is performing a full re-layout of its elements. If this texture + * atlas' dimensions are changing, the specified backing images refer to separate instances with different + * dimensions. If this texture atlas is performing a re-layout in place, the specified backing images refer to the + * same instance. In either case, the specified rectangles correspond to the element's location in the old backing + * image and new backing image. + * + * @param oldBackingImage the backing image corresponding to the previous layout. + * @param oldRect the element's location in oldBackingImage. + * @param newBackingImage the backing image corresponding to the new layout. + * @param newRect the element's location in newBackingImage. + */ + protected void moveEntry(BufferedImage oldBackingImage, Rect oldRect, BufferedImage newBackingImage, Rect newRect) + { + // Note that there is no need to update the rectangle instance associated with the entry for this rectangle. The + // JOGL rectangle packer automatically takes care of updating the rectangle for us. + + this.g.setComposite(AlphaComposite.Src); // Replace destination pixels with the clear color (disables blending). + + if (oldBackingImage == newBackingImage) + { + // The backing image has not changed. Move the entry's rectangle from its old location to its new location. + this.g.copyArea(oldRect.x(), oldRect.y(), oldRect.w(), oldRect.h(), // x, y, width, height + newRect.x() - oldRect.x(), newRect.y() - oldRect.y()); // dx, dy + } + else + { + // The backing image is changing. Copy the entry from its location in the old backing images to its location + // in the new backing image. + this.g.drawImage(oldBackingImage, + // dstX1, dstY1, dstX2, dstY2 + newRect.x(), newRect.y(), newRect.x() + newRect.w(), newRect.y() + newRect.h(), + // srcX1, srcY1, srcX2, srcY2 + oldRect.x(), oldRect.y(), oldRect.x() + oldRect.w(), oldRect.y() + oldRect.h(), + null); + } + } + + /** + * Marks the specified entry as used by setting its last used time to the current time in nanoseconds. + * + * @param entry the entry who's last used time is marked. + */ + protected void markUsed(Entry entry) + { + entry.lastUsed = System.nanoTime(); + } + + /** + * Removes the oldest entry from this texture atlas. This does nothing if this texture atlas is empty. + * + * @return true if this removed an entry, and false if there are no entries to remove. + */ + protected boolean removeLeastRecentlyUsedEntry() + { + if (this.entryMap.isEmpty()) + return false; + + Entry[] timeOrderedEntries = new Entry[this.entryMap.size()]; + Arrays.sort(this.entryMap.values().toArray(timeOrderedEntries)); + + Entry entryToRemove = timeOrderedEntries[0]; + this.entryMap.remove(entryToRemove.key); + this.doRemove(entryToRemove); + + return true; + } + + /** + * Returns the region of this texture atlas' backing image that is not currently synchronized with the OpenGL + * texture. + * + * @return the region of this texture atlas that must be synchronized. + */ + protected Rectangle getDirtyRect() + { + return this.dirtyRect; + } + + /** + * Marks a region of this texture atlas' backing image as needing to be synchronized with the OpenGL texture. If + * there is already a dirty region, the final dirty region is the union of the two. + * + * @param x the X coordinate of the region's upper-left corner, in pixels. + * @param y the Y coordinate of the region's upper-left corner, in pixels. + * @param width the region's width, in pixels. + * @param height the region's height, in pixels. + */ + protected void markDirty(int x, int y, int width, int height) + { + Rectangle rect = new Rectangle(x, y, width, height); + + if (this.dirtyRect == null) + this.dirtyRect = rect; + else + this.dirtyRect.add(rect); + } + + /** + * Removes any regions in this texture atlas' backing image previously marked as needing to be synchronized with the + * OpenGL texture. + */ + protected void clearDirtyRect() + { + this.dirtyRect = null; + } + + /** + * Indicates the OpenGL {@link Texture} associated with this texture atlas. + * + * @param dc the current draw context. + * + * @return this instance's OpenGL texture, or null if the texture does not currently exist. + */ + protected Texture getTexture(DrawContext dc) + { + return dc.getTextureCache().getTexture(this.textureKey); + } + + /** + * Specifies the OpenGL {@link Texture} associated with this texture atlas. + * + * @param dc the current draw context. + * @param texture this instance's OpenGL texture, or null to specify that this texture atlas has no + * texture. + */ + protected void setTexture(DrawContext dc, Texture texture) + { + dc.getTextureCache().put(this.textureKey, texture); + } + + /** + * Removes textures corresponding to this texture atlas' disposed backing images from the draw context's GPU + * resource cache. While disposed backing textures would eventually be evicted by the GPU resource cache, explicitly + * disposing them avoids polluting the GPU resource cache with orphaned textures that correspond only to this + * texture atlas. + * + * @param dc the draw context containing the GPU resource cache to remove textures from. + */ + protected void disposeOldTextures(DrawContext dc) + { + // Process each key in the disposedTextureKeys queue. Since TextureAtlas keys are unique to each instance, the + // texture keys are not shared with any other object, and therefore are orphaned once they're unused. We + // explicitly remove them from the texture cache to ensure that this texture atlas uses a minimal amount of + // texture memory. + Object key; + while ((key = this.disposedTextureKeys.poll()) != null) + { + // The key may never have been be associated with a texture if this texture atlas was expanded or contracted + // more than once between calls to bind. In this case we just ignore the disposed key and continue. + if (dc.getTextureCache().contains(key)) + dc.getTextureCache().remove(key); + } + } + + /** + * Synchronizes this texture atlas's backing image with its OpenGL texture, creating an OpenGL texture as necessary. + * This attempts to minimize transfer between Java and OpenGL by loading the smallest possible portion of the + * backing image into the OpenGL texture. + * + * @param dc the current draw context. + * + * @return this texture atlas' OpenGL texture. + */ + protected Texture syncTexture(DrawContext dc) + { + Texture texture = this.getTexture(dc); + + if (texture == null) + { + // This texture atlas' OpenGL texture does not exist on the specified draw context. Load the entire backing + // image into a new texture and use that as this texture atlas' OpenGL texture. + texture = this.makeTextureWithBackingImage(dc); + } + else if (this.getDirtyRect() != null) + { + // A region of this texture atlas' OpenGL texture is out-of-sync; load only the necessary portion of the + // backing image into the texture. + texture = this.updateTextureWithSubImage(dc, this.getDirtyRect()); + } + + // Clear the dirty rectangle to indicate that this texture atlas' backing image and texture are synchronized. + this.clearDirtyRect(); + + return texture; + } + + /** + * Creates an OpenGL texture by loading this texture atlas's backing image into a new texture with the same + * dimensions. + * + * @param dc the current draw context. + * + * @return a new OpenGL texture containing the data from this texture atlas' backing image. + */ + protected Texture makeTextureWithBackingImage(DrawContext dc) + { + BufferedImage backingImage = (BufferedImage) this.rectPacker.getBackingStore(); + Texture texture = TextureIO.newTexture(backingImage, this.isUseMipMaps()); + + this.setTexture(dc, texture); + this.setTextureParameters(dc); + + return texture; + } + + /** + * Loads a sub-region of this texture atlas' backing image into its OpenGL texture. This does nothing and returns + * code null if this texture atlas' does not have an OpenGL texture. + * + * @param dc the current draw context. + * @param rect the rectangle to load. + * + * @return this texture atlas' OpenGL texture, or null if this texture atlas' does not have an OpenGL + * texture. + */ + protected Texture updateTextureWithSubImage(DrawContext dc, Rectangle rect) + { + Texture texture = this.getTexture(dc); + if (texture == null) // This should never happen, but we check anyway. + { + String msg = Logging.getMessage("nullValue.TextureIsNull"); + Logging.logger().warning(msg); + return null; + } + + if (!this.isUseMipMaps() || texture.isUsingAutoMipmapGeneration()) + { + // If we're either not using mip-maps or we have automatic mip-map generation, then load the sub-image + // corresponding to the specified rectangle into the OpenGL texture. Note that the x and y coordinates of + // the dirty region do not need to be translated because the image and texture share the same coordinate + // system. + BufferedImage backingImage = (BufferedImage) this.rectPacker.getBackingStore(); + BufferedImage subImage = backingImage.getSubimage(rect.x, rect.y, rect.width, rect.height); + TextureData subTextureData = TextureIO.newTextureData(subImage, false); // No need for sub-image mip-maps. + texture.updateSubImage(subTextureData, 0, rect.x, rect.y); + } + else + { + // If we're using mip-maps but do not have automatic mip-map generation, we must load the entire image into + // the texture in order to force JOGL to recompute the mip-map data for all levels in Java. We must also + // respecify the texture parameters, because Texture.updateImage overwrites the texture parameters with + // default values. + BufferedImage backingImage = (BufferedImage) this.rectPacker.getBackingStore(); + texture.updateImage(TextureIO.newTextureData(backingImage, this.isUseMipMaps())); + this.setTextureParameters(dc); + } + + return texture; + } + + /** + * Specifies the OpenGL texture parameters associated with this texture atlas' OpenGL texture. This is always called + * when the OpenGL texture is bound to the draw context's GLContext. + * + * @param dc the current draw context. + */ + protected void setTextureParameters(DrawContext dc) + { + GL gl = dc.getGL(); + + // The JOGL Texture class specifies appropriate default values for the following OpenGL texture parameters: + // - GL_TEXTURE_MIN_FILTER + // - GL_TEXTURE_MAG_FILTER + // - GL_TEXTURE_WRAP_S + // - GL_TEXTURE_WRAP_T + + if (this.isUseMipMaps() && this.isUseAnisotropy()) + { + double maxAnisotropy = dc.getGLRuntimeCapabilities().getMaxTextureAnisotropy(); + if (dc.getGLRuntimeCapabilities().isUseAnisotropicTextureFilter() && maxAnisotropy >= 2.0) + { + gl.glTexParameterf(GL.GL_TEXTURE_2D, GL.GL_TEXTURE_MAX_ANISOTROPY_EXT, (float) maxAnisotropy); + } + } + } +} diff --git a/WorldWind/src/gov/nasa/worldwind/util/TextureAtlasElement.java b/WorldWind/src/gov/nasa/worldwind/util/TextureAtlasElement.java new file mode 100644 index 000000000..76a24d4a5 --- /dev/null +++ b/WorldWind/src/gov/nasa/worldwind/util/TextureAtlasElement.java @@ -0,0 +1,472 @@ +/* + * Copyright (C) 2011 United States Government as represented by the Administrator of the + * National Aeronautics and Space Administration. + * All Rights Reserved. + */ +package gov.nasa.worldwind.util; + +import com.sun.opengl.util.texture.TextureCoords; +import gov.nasa.worldwind.*; +import gov.nasa.worldwind.avlist.AVKey; +import gov.nasa.worldwind.render.DrawContext; + +import javax.imageio.ImageIO; +import java.awt.*; +import java.awt.image.*; +import java.beans.*; +import java.net.URL; + +/** + * Represents a texture composed of a single element of a {@link TextureAtlas}. + *

+ * TextureAtlasElement performs lazy retrieval and loading, attempting to load the image source and add it to the atlas + * ony when the {@link #load(gov.nasa.worldwind.render.DrawContext)} method is called. If the image source is a {@link + * BufferedImage} it is added to the atlas immediately when load is called. If the image source is a local + * file or a remote stream (URL), retrieval and loading is performed on a separate thread from the EDT. + * + * @author dcollins + * @version $Id$ + */ +public class TextureAtlasElement implements Disposable +{ + /** Indicates the texture atlas this element belongs to. Specified during construction. */ + protected TextureAtlas atlas; + /** Indicates the original image source associated with this element. Specified during construction. */ + protected Object imageSource; + /** + * The BufferedImage created as the image source is read. This intermediate field is necessary because the image + * source is read on a non-EDT thread, but changes to the texture atlas must be performed on the EDT. This is set to + * null once the image is loaded into the texture atlas. This field is volatile in order + * to synchronize atomic access among threads. This field is not used if the image source is + * BufferedImage. + */ + protected volatile BufferedImage image; + /** + * Indicates that image initialization failed. This element should not be used if true. Initially + * false. + */ + protected boolean imageInitializationFailed; + /** + * The object to notify when the image is eventually loaded in memory. This is either the current layer or the layer + * list at the time the image source is requested. The latter is used when the image source is requested during + * ordered rendering mode, and the current layer is null. This set to null once the image + * is loaded into the texture atlas. + */ + protected PropertyChangeListener listener; + + /** + * Creates a new texture atlas element with the specified atlas and image source. + * + * @param atlas the texture atlas this element belongs to. + * @param imageSource a general image source. The source type may be one of the following:

  • a {@link + * URL}
  • an {@link java.io.InputStream}
  • a {@link java.io.File}
  • a {@link + * String} containing a valid URL description or a file or resource name available on the + * classpath.
+ * + * @throws IllegalArgumentException if either the atlas or the image source is null. + */ + public TextureAtlasElement(TextureAtlas atlas, Object imageSource) + { + if (atlas == null) + { + String msg = Logging.getMessage("nullValue.AtlasIsNull"); + Logging.logger().severe(msg); + throw new IllegalArgumentException(msg); + } + + if (WWUtil.isEmpty(imageSource)) + { + String msg = Logging.getMessage("nullValue.ImageSource"); + Logging.logger().severe(msg); + throw new IllegalArgumentException(msg); + } + + this.atlas = atlas; + this.imageSource = imageSource; + } + + /** + * Indicates the texture atlas this element belongs to. + * + * @return this element's texture atlas. + */ + public TextureAtlas getTextureAtlas() + { + return this.atlas; + } + + /** + * Indicates the original image source associated with this element. + * + * @return this element's image source. + */ + public Object getImageSource() + { + return this.imageSource; + } + + /** + * Indicates whether this element's image source is a BufferedImage. + * + * @return true if this element's image source is a BufferedImage, and false otherwise. + */ + protected boolean isBufferedImageSource() + { + return this.getImageSource() instanceof BufferedImage; + } + + /** + * Indicates the image created as the image source is read. + * + * @return this element's image. + * + * @see #setImage(java.awt.image.BufferedImage) + */ + protected BufferedImage getImage() + { + return this.image; + } + + /** + * Specifies the image created as the image source is read. This intermediate field is necessary because the image + * source is read on a non-EDT thread, but changes to the texture atlas must be performed on the EDT. This field is + * volatile in order to synchronize atomic access among threads. + * + * @param image this element's image. + */ + protected void setImage(BufferedImage image) + { + this.image = image; + } + + /** + * Returns the image dimensions associated with this texture atlas element. Always call load before + * calling this method to ensure that the element is loaded into its texture atlas. + * + * @return the image dimensions associated with this texture atlas element, or null if this texture + * atlas element has not yet loaded, or has failed to load. + * + * @see #load(gov.nasa.worldwind.render.DrawContext) + */ + public Dimension getSize() + { + return this.getTextureAtlas().getSize(this.getImageSource()); + } + + /** + * Returns the OpenGL texture coordinates associated this texture atlas element. Always call load + * before calling this method to ensure that the element is loaded into its texture atlas. + * + * @return the OpenGL texture coordinates corresponding this texture atlas element, or null if this + * texture atlas element has not yet loaded, or has failed to load. + * + * @see #load(gov.nasa.worldwind.render.DrawContext) + */ + public TextureCoords getTexCoords() + { + return this.getTextureAtlas().getTexCoords(this.getImageSource()); + } + + /** + * Indicates whether this element's image failed to load. This element should not be used if true. + * + * @return true if this element's image failed to load, and false otherwise. + */ + public boolean isImageInitializationFailed() + { + return this.imageInitializationFailed; + } + + /** + * Loads this element's image and adds it to the texture atlas once the image is loaded. If the image is not yet + * loaded this initiates image source retrieval in a separate thread. This does nothing if the texture atlas already + * contains this element, or if this element's image failed to load in an earlier attempt. + * + * @param dc the current draw context. Used to generate a repaint event when the image source retrieval completes. + * + * @return true if this element's image is loaded into the texture atlas, and false + * otherwise. + */ + public boolean load(DrawContext dc) + { + if (dc == null) + { + String msg = Logging.getMessage("nullValue.DrawContextIsNull"); + Logging.logger().severe(msg); + throw new IllegalArgumentException(msg); + } + + if (this.isImageInitializationFailed()) + return false; + + // The atlas already contains an entry for this element then just return true. + if (this.getTextureAtlas().contains(this.getImageSource())) + return true; + + // The atlas does not contain an entry for this element. Issue a request for this element's image if it does not + // exist, or load it into the atlas if it does. In this case we return true if this element was successfully + // loaded into the atlas, and false otherwise. + return this.requestImage(dc); + } + + /** Removes this element's image from its texture atlas, if necessary. */ + public void dispose() + { + if (this.getTextureAtlas().contains(this.getImageSource())) + this.getTextureAtlas().remove(this.getImageSource()); + + this.setImage(null); + } + + /** + * Indicates whether another texture atlas element is equivalent to this one. This tests equality using the image + * source of each element. + * + * @param o the object to test. + * + * @return true if the specified object is a TextureAtlasElement, and its image source is equivalent to + * this element's image source. + */ + @Override + public boolean equals(Object o) + { + if (this == o) + return true; + if (o == null || this.getClass() != o.getClass()) + return false; + + TextureAtlasElement that = (TextureAtlasElement) o; + return this.imageSource != null ? this.imageSource.equals(that.imageSource) : that.imageSource == null; + } + + /** Returns the hash code for this texture atlas element's image source. */ + @Override + public int hashCode() + { + return this.imageSource != null ? this.imageSource.hashCode() : 0; + } + + /** Returns the string representation of this texture atlas element's image source. */ + @Override + public String toString() + { + return this.imageSource != null ? this.imageSource.toString() : null; + } + + /** + * Requests that this element's image source be loaded into its texture atlas. If the image source is a + * BufferedImage, this immediately loads it into the texture atlas and returns true. Otherwise, this + * initiates the retrieval of this element's image source in a separate thread and returns false. Once + * the image source is retrieved, a subsequent invocation of this method loads it into the texture atlas and returns + * true. + * + * @param dc the current draw context. Used to generate a repaint event when the image source retrieval completes. + * + * @return true if this element's image is loaded into the texture atlas, and false + * otherwise. + */ + protected boolean requestImage(DrawContext dc) + { + // If the image source is already a buffered image, assign it to this element's image and let the subsequent + // logic in this method take care of adding it to the atlas. + if (this.isBufferedImageSource()) + this.setImage((BufferedImage) this.getImageSource()); + + if (this.getImage() != null && !this.getTextureAtlas().contains(this.getImageSource())) + return this.addAtlasImage(); + + if (WorldWind.getTaskService().isFull()) + return false; + + Runnable task = this.createRequestTask(); + if (WorldWind.getTaskService().contains(task)) + return false; + + // Use either the current layer or the layer list as the listener to notify when the request completes. The + // latter is used when the image source is requested during ordered rendering mode, and the current layer is + // null. + this.listener = dc.getCurrentLayer() != null ? dc.getCurrentLayer() : dc.getLayers(); + + WorldWind.getTaskService().addTask(task); + + return false; + } + + /** + * Adds this element's image source into its texture atlas. This throws an exception if this element's image source + * is not loaded. + * + * @return true if this element's image has been added to the texture atlas, and false + * otherwise. + */ + protected boolean addAtlasImage() + { + if (this.getImage() == null) + { + String msg = Logging.getMessage("nullValue.ImageIsNull"); + Logging.logger().severe(msg); + throw new IllegalStateException(msg); + } + + try + { + // Place this element's image in the atlas, then release our reference to the image. + this.getTextureAtlas().add(this.getImageSource(), this.getImage()); + this.setImage(null); + } + catch (Exception e) + { + String msg = Logging.getMessage("TextureAtlas.ExceptionAddingImage", this.getImageSource().toString()); + Logging.logger().log(java.util.logging.Level.SEVERE, msg, e); + this.imageInitializationFailed = true; + return false; + } + + return true; + } + + /** + * Returns an object that implements the Runnable interface, and who's run method retrieves and loads + * this element's image source. + * + * @return a new request task that retrieves and loads this element's image source. + */ + protected Runnable createRequestTask() + { + return new RequestTask(this); + } + + /** + * Loads this element's image source into its image property. If the image source is a remote resource, + * this initiates a request for it and returns null. + * + * @return true if the image source has been loaded successfully, and false otherwise. + */ + protected boolean loadImage() + { + URL fileUrl = WorldWind.getDataFileStore().requestFile(this.getImageSource().toString()); + if (fileUrl != null) + { + BufferedImage image = this.readImage(fileUrl); + if (image != null) + this.setImage(image); + } + + return this.getImage() != null; + } + + /** + * Reads and returns the specified image URL as a BufferedImage. + * + * @param fileUrl the image URL to read. + * + * @return the image URL as a BufferedImage, or null if the image could not be read. + */ + protected BufferedImage readImage(URL fileUrl) + { + try + { + return ImageIO.read(fileUrl); + } + catch (Exception e) + { + String msg = Logging.getMessage("generic.ExceptionAttemptingToReadImageFile", + this.getImageSource().toString()); + Logging.logger().log(java.util.logging.Level.SEVERE, msg, e); + this.imageInitializationFailed = true; + return null; + } + } + + /** + * Notifies this texture atlas element's listener that image loading has completed. This does nothing if this + * texture atlas element has no listener. + */ + protected void notifyImageLoaded() + { + if (this.listener != null) + { + this.listener.propertyChange(new PropertyChangeEvent(this, AVKey.IMAGE, null, this)); + this.listener = null; // Forget the listener to avoid dangling references. + } + } + + /** + * RequestTask is an of the Runnable interface who's run method retrieves and loads this element's + * image source. + */ + protected static class RequestTask implements Runnable + { + /** The texture atlas element associated with this request task. Specified during construction. */ + protected TextureAtlasElement elem; + + /** + * Constructs a new request task with the specified texture atlas element, but otherwise does nothing. Calling + * the new request tasks run method causes this to retrieve and load the specified element's image + * source. + * + * @param elem the texture atlas element who's image source is retrieved and loaded. + * + * @throws IllegalArgumentException if the element is null. + */ + protected RequestTask(TextureAtlasElement elem) + { + if (elem == null) + { + String message = Logging.getMessage("nullValue.ElementIsNull"); + Logging.logger().severe(message); + throw new IllegalArgumentException(message); + } + + this.elem = elem; + } + + /** + * Retrieves and loads the image source from this request task's texture atlas element, and notifies the element + * when the load completes. This does nothing if the current thread has been interrupted. + */ + public void run() + { + if (Thread.currentThread().isInterrupted()) + return; // The task was cancelled because it's a duplicate or for some other reason. + + if (this.elem.loadImage()) + this.elem.notifyImageLoaded(); + } + + /** + * Indicates whether another request task is equivalent to this one. This tests equality using the texture atlas + * element source of each task. + * + * @param o the object to test. + * + * @return true if the specified object is a RequestTask, and its texture atlas element is + * equivalent to this task's texture atlas element. + */ + @Override + public boolean equals(Object o) + { + if (this == o) + return true; + if (o == null || this.getClass() != o.getClass()) + return false; + + RequestTask that = (RequestTask) o; + return this.elem.equals(that.elem); + } + + /** Returns the hash code for this request task's texture atlas element. */ + @Override + public int hashCode() + { + return this.elem.hashCode(); + } + + /** Returns the string representation of this request task's texture atlas element. */ + @Override + public String toString() + { + return this.elem.toString(); + } + } +}