All Downloads are FREE. Search and download functionalities are using the official Maven repository.

src.gov.nasa.worldwind.render.LazilyLoadedTexture Maven / Gradle / Ivy

Go to download

World Wind is a collection of components that interactively display 3D geographic information within Java applications or applets.

There is a newer version: 2.0.0-986
Show newest version
/*
 * Copyright (C) 2012 United States Government as represented by the Administrator of the
 * National Aeronautics and Space Administration.
 * All Rights Reserved.
 */

package gov.nasa.worldwind.render;

import com.jogamp.opengl.util.texture.*;
import com.jogamp.opengl.util.texture.awt.AWTTextureIO;
import gov.nasa.worldwind.*;
import gov.nasa.worldwind.avlist.*;
import gov.nasa.worldwind.cache.FileStore;
import gov.nasa.worldwind.util.*;

import javax.media.opengl.*;
import java.awt.image.*;
import java.beans.*;
import java.net.URL;

/**
 * Represents a texture derived from a lazily loaded image source such as an image file or a {@link
 * java.awt.image.BufferedImage}.
 * 

* The interface contains a method, {@link #isTextureInitializationFailed()} to determine whether the instance failed to * convert an image source to a texture. If such a failure occurs, the method returns true and no further attempts are * made to create the texture. *

* This class performs lazy retrieval and loading of an image source, attempting to retrieve and load the image source * only when the {@link #bind(DrawContext)} or {@link #applyInternalTransform(DrawContext)} methods are called. If the * image source is a {@link BufferedImage} the associated {@link Texture} object is created and available immediately * when bind or applyInternalTransform are called. If the image source is a local file or * remote stream (URL), retrieval and loading is performed on a separate thread from the EDT. * * @author tag * @version $Id: LazilyLoadedTexture.java 1171 2013-02-11 21:45:02Z dcollins $ */ public class LazilyLoadedTexture extends AVListImpl implements WWTexture { /** The original image source specified at construction. */ protected Object imageSource; /** The mip-map flag specified at construction. */ protected boolean useMipMaps; /** The current anisotropy flag. */ protected boolean useAnisotropy = true; /** The texture width, if the width is known. Otherwise it's -1. */ protected Integer width; /** The texture height, if the height is known. Otherwise it's -1. */ protected Integer height; /** The texture's texture coordinates, as determined by JOGL when the texture is created. */ protected TextureCoords texCoords; /** * The texture data created as the image source is read. It's removed - set to null - once the textures is fully * created. This intermediate texture data is necessary because the image source is read in a non-EDT thread. 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 TextureData textureData; // if non-null, then must be converted to a Texture /** Indicates that texture initialization failed. This texture should not be used if true. */ protected boolean textureInitializationFailed = false; /** Indicates whether the image read from the image source has mip-map data. */ protected boolean hasMipmapData = false; /** Identifies the {@link gov.nasa.worldwind.cache.FileStore} of the supporting file cache for this model. */ protected FileStore fileStore = WorldWind.getDataFileStore(); /** Provides a semaphore to synchronize access to the texture file if duplicate request tasks are active. */ protected final Object fileLock = new Object(); /** * The object to notify when an image is eventually loaded in memory. The current layer at the time the image source * is requested is assigned to this field. */ protected PropertyChangeListener listener; /** * Constructs a texture object for a specified image source. Requests that mip-maps be used. * * @param imageSource the source of the image, either a file path {@link String} or a {@link * java.awt.image.BufferedImage}. * * @throws IllegalArgumentException if the imageSource is null. */ public LazilyLoadedTexture(Object imageSource) { this(imageSource, true); } /** * Constructs a texture object for a specified image source. * * @param imageSource the source of the image, either a file path {@link String} or a {@link * java.awt.image.BufferedImage}. * @param useMipMaps Indicates whether to generate and use mip-maps for the image. * * @throws IllegalArgumentException if the imageSource is null. */ public LazilyLoadedTexture(Object imageSource, boolean useMipMaps) { initialize(imageSource, useMipMaps, null); } /** * Initializes this object's fields during construction. * * @param imageSource the image source. * @param useMipMaps the mip-map flag. * @param listener the change listener. * * @throws IllegalArgumentException if the image source is null. */ protected void initialize(Object imageSource, boolean useMipMaps, PropertyChangeListener listener) { if (imageSource == null) { String message = Logging.getMessage("nullValue.ImageSource"); Logging.logger().severe(message); throw new IllegalArgumentException(message); } this.imageSource = imageSource; this.useMipMaps = useMipMaps; if (listener != null) this.addPropertyChangeListener(listener); } public Object getImageSource() { return this.imageSource; } /** * Indicates whether the image source is a BufferedImage * * @return true if the image source is a BufferedImage, otherwise false. */ protected boolean isBufferedImageSource() { return this.getImageSource() instanceof BufferedImage; } /** * Indicates the texture's width, which is the same as the image source's width. The width is known only after the * image source has been retrieved from disk or network and read as TextureData. It's value is -1 until * then. * * @return the texture's width if the texture has been retrieved, otherwise -1. */ public int getWidth() { return this.width != null ? this.width : -1; } /** * Indicates the texture's height, which is the same as the image source's height. The height is known only after * the image source has been retrieved from disk or network and read as TextureData. It's value is -1 * until then. * * @return the texture's height if the texture has been retrieved, otherwise -1. */ public int getHeight() { return this.height != null ? this.height : -1; } /** * {@inheritDoc} *

* This method behaves identically to {@link #getWidth()}. The DrawContext argument is not used. * * @param dc this parameter is not used by this class. * * @return the texture's width if the texture has been retrieved, otherwise -1. */ public int getWidth(DrawContext dc) { return this.getWidth(); } /** * {@inheritDoc} *

* This method behaves identically to {@link #getHeight()}. The DrawContext argument is not used. * * @param dc this parameter is not used by this class. * * @return the texture's height if the texture has been retrieved, otherwise -1. */ public int getHeight(DrawContext dc) { return this.getHeight(); } /** * Indicates whether the texture should use mip-maps. If they are not available in the source image they are * created. * * @return true if mip-maps are used, false if not. */ public boolean isUseMipMaps() { return this.useMipMaps; } public TextureCoords getTexCoords() { return this.texCoords; } public boolean isTextureCurrent(DrawContext dc) { return this.getTexture(dc) != null; } /** * Indicates whether texture anisotropy is applied to the texture when rendered. * * @return useAnisotropy true if anisotropy is to be applied, otherwise false. */ public boolean isUseAnisotropy() { return this.useAnisotropy; } /** * Specifies whether texture anisotropy is applied to the texture when rendered. * * @param useAnisotropy true if anisotropy is to be applied, otherwise false. */ public void setUseAnisotropy(boolean useAnisotropy) { this.useAnisotropy = useAnisotropy; } /** * Indicates whether an attempt was made to retrieve and read the texture but it failed. If this flag is true, this * texture should not be used. * * @return true if texture retrieval or creation failed, otherwise true, even if the image source has not yet been * retrieved. */ public boolean isTextureInitializationFailed() { return this.textureInitializationFailed; } /** * Returns the {@link Texture} associated with this instance. * * @param dc the current draw context. * * @return this instance's texture, or null if the texture does not currently exist. */ protected Texture getTexture(DrawContext dc) { if (this.getImageSource() == null) return null; Texture texture = dc.getTextureCache().getTexture(this.getImageSource()); if (this.width == null && texture != null) { this.width = texture.getWidth(); this.height = texture.getHeight(); this.texCoords = texture.getImageTexCoords(); } return texture; } /** * Returns this texture's texture data if it has been retrieved but a Texture has not yet been created * for it. *

* If this object's texture data field is non-null, a new texture is created from the texture data when the tile is * next bound or otherwise initialized. This object's texture data field is then set to null. * * @return the texture data, which may be null indicating that the image source has not been read or that a texture * has been created. */ protected TextureData getTextureData() { return this.textureData; } /** * Specifies texture data for the tile. If texture data is non-null, a new texture is created from the texture data * when the tile is next bound. *

* When a texture is created from the texture data, the texture data field is set to null to indicate that the data * has been converted to a texture and its resources may be released. * * @param textureData the texture data, which may be null. */ protected void setTextureData(TextureData textureData) { this.textureData = textureData; if (textureData != null && textureData.getMipmapData() != null) this.hasMipmapData = true; } /** * Binds this instance's {@link Texture} to the GLContext if the texture has been created, otherwise * initiates image source retrieval and texture creation in a separate thread. * * @param dc the current draw context. * * @return true if the texture was bound, otherwise false. */ public boolean bind(DrawContext dc) { if (this.isTextureInitializationFailed()) return false; if (dc == null) { String message = Logging.getMessage("nullValue.DrawContextIsNull"); Logging.logger().severe(message); throw new IllegalStateException(message); } Texture texture = this.getTexture(dc); if (texture == null) texture = this.requestTexture(dc); if (texture != null) { texture.bind(dc.getGL()); return true; } else { return false; } } public void applyInternalTransform(DrawContext dc) { if (dc == null) { String message = Logging.getMessage("nullValue.DrawContextIsNull"); Logging.logger().severe(message); throw new IllegalStateException(message); } Texture texture = this.getTexture(dc); if (texture == null) texture = this.requestTexture(dc); if (texture == null) return; if (texture.getMustFlipVertically()) { GL2 gl = dc.getGL().getGL2(); // GL initialization checks for GL2 compatibility. gl.glMatrixMode(GL2.GL_TEXTURE); gl.glLoadIdentity(); gl.glScaled(1, -1, 1); gl.glTranslated(0, -1, 0); } } /** * If this instance's image source is a BufferedImage, creates and returns the texture, otherwise * creates a task in a separate thread to retrieve it from its local or remote location. * * @param dc the current draw context. * * @return the new texture, or null if the texture is not yet available. */ protected Texture requestTexture(DrawContext dc) { if (this.isBufferedImageSource()) return this.makeBufferedImageTexture(dc); if (this.getTextureData() != null && this.getTexture(dc) == null) return this.makeTextureFromTextureData(dc); if (WorldWind.getTaskService().isFull()) return null; Runnable task = this.createRequestTask(); if (WorldWind.getTaskService().contains(task)) return null; // 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 and the current layer is null. this.listener = dc.getCurrentLayer() != null ? dc.getCurrentLayer() : dc.getLayers(); WorldWind.getTaskService().addTask(task); return null; } /** * Returns an object that implements the Runnable interface, and who's run method retrieves and loads * this texture's image source. * * @return a new request task that retrieves and loads this texture's image source. */ protected Runnable createRequestTask() { return new RequestTask(this); } /** * Creates this instance's {@link Texture} if the image source is a BufferedImage. * * @param dc the current draw context. * * @return the newly created texture, or null if the texture was not created. * * @throws IllegalStateException if the image source is null or not a BufferedImage. */ protected Texture makeBufferedImageTexture(DrawContext dc) { if (this.getImageSource() == null || !(this.getImageSource() instanceof BufferedImage)) { String message = Logging.getMessage("generic.NotABufferedImage"); Logging.logger().severe(message); throw new IllegalStateException(message); } try { TextureData td = AWTTextureIO.newTextureData(Configuration.getMaxCompatibleGLProfile(), (BufferedImage) this.getImageSource(), this.isUseMipMaps()); if (td == null) return null; this.setTextureData(td); return this.makeTextureFromTextureData(dc); } catch (Exception e) { String msg = Logging.getMessage("generic.IOExceptionDuringTextureInitialization"); Logging.logger().log(java.util.logging.Level.SEVERE, msg, e); this.textureInitializationFailed = true; return null; } } /** * Creates a {@link Texture} from this instance's {@link TextureData} if the TextureData exists. * * @param dc the current draw context. * * @return the newly created texture, or null if this instance has no current TextureData or if texture * creation failed. */ protected Texture makeTextureFromTextureData(DrawContext dc) { if (dc == null) { String message = Logging.getMessage("nullValue.DrawContextIsNull"); Logging.logger().severe(message); throw new IllegalStateException(message); } if (this.getTextureData() == null) // texture not in cache yet texture data is null, can't initialize { String msg = Logging.getMessage("nullValue.TextureDataIsNull"); Logging.logger().severe(msg); throw new IllegalStateException(msg); } try { Texture texture = TextureIO.newTexture(this.getTextureData()); if (texture == null) { this.textureInitializationFailed = true; return null; } this.width = texture.getWidth(); this.height = texture.getHeight(); this.texCoords = texture.getImageTexCoords(); this.setTextureParameters(dc, texture); // Cache the texture and release the texture data. dc.getTextureCache().put(this.getImageSource(), texture); this.setTextureData(null); return texture; } catch (Exception e) { String name = this.isBufferedImageSource() ? "BufferedImage" : this.getImageSource().toString(); String msg = Logging.getMessage("generic.ExceptionAttemptingToCreateTexture", name); Logging.logger().log(java.util.logging.Level.SEVERE, msg, e); return null; } } /** * Sets a specified texture's OpenGL Texture parameters. * * @param dc the current draw context. * @param texture the texture whose parameters to set. */ protected void setTextureParameters(DrawContext dc, Texture texture) { // Enable the appropriate mip-mapping texture filters if the caller has specified that mip-mapping should be // enabled, and the texture itself supports mip-mapping. boolean useMipMapFilter = this.useMipMaps && (this.getTextureData().getMipmapData() != null || texture.isUsingAutoMipmapGeneration()); GL gl = dc.getGL(); gl.glTexParameteri(GL.GL_TEXTURE_2D, GL.GL_TEXTURE_MIN_FILTER, useMipMapFilter ? GL.GL_LINEAR_MIPMAP_LINEAR : GL.GL_LINEAR); gl.glTexParameteri(GL.GL_TEXTURE_2D, GL.GL_TEXTURE_MAG_FILTER, GL.GL_LINEAR); gl.glTexParameteri(GL.GL_TEXTURE_2D, GL.GL_TEXTURE_WRAP_S, GL.GL_CLAMP_TO_EDGE); gl.glTexParameteri(GL.GL_TEXTURE_2D, GL.GL_TEXTURE_WRAP_T, GL.GL_CLAMP_TO_EDGE); if (this.isUseAnisotropy() && useMipMapFilter) { 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); } } } protected void notifyTextureLoaded() { if (this.listener != null) { this.listener.propertyChange(new PropertyChangeEvent(this, AVKey.TEXTURE, null, this)); this.listener = null; // forget the listener to avoid dangling references } } /** Attempts to find this texture's image file locally, and if that fails attempts to find it remotely. */ protected static class RequestTask implements Runnable { /** The BasicWWTexture associated with this request. */ protected final LazilyLoadedTexture wwTexture; /** * Construct a request task for a specified BasicWWTexture. * * @param wwTexture the texture object for which to construct the request task. */ protected RequestTask(LazilyLoadedTexture wwTexture) { if (wwTexture == null) { String message = Logging.getMessage("nullValue.TextureIsNull"); Logging.logger().severe(message); throw new IllegalArgumentException(message); } this.wwTexture = wwTexture; } public void run() { if (Thread.currentThread().isInterrupted()) return; // the task was cancelled because it's a duplicate or for some other reason URL fileUrl = this.wwTexture.fileStore.requestFile(this.wwTexture.getImageSource().toString()); if (fileUrl != null) { if (this.wwTexture.loadTextureData(fileUrl)) { this.wwTexture.notifyTextureLoaded(); } } } public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; final RequestTask that = (RequestTask) o; return !(this.wwTexture != null ? !this.wwTexture.equals(that.wwTexture) : that.wwTexture != null); } public int hashCode() { return (this.wwTexture != null ? this.wwTexture.hashCode() : 0); } public String toString() { return this.wwTexture.getImageSource().toString(); } } /** * Loads the image from disk into memory. If successful, texture data is created and available via {@link * #getTextureData()}. * * @param fileUrl the URL of the image file. * * @return true if the image was successfully loaded, otherwise false. */ protected boolean loadTextureData(URL fileUrl) { TextureData td; synchronized (this.fileLock) { td = readImage(fileUrl); if (td != null) this.setTextureData(td); } return this.getTextureData() != null; } /** * Reads and returns a {@link TextureData} for an image from a specified file URL. * * @param fileUrl the URL of the image file to read. * * @return a TextureData instance for the image. */ protected TextureData readImage(URL fileUrl) { try { return OGLUtil.newTextureData(Configuration.getMaxCompatibleGLProfile(), fileUrl, this.isUseMipMaps()); } catch (Exception e) { String msg = Logging.getMessage("layers.TextureLayer.ExceptionAttemptingToReadTextureFile", this.getImageSource()); Logging.logger().log(java.util.logging.Level.SEVERE, msg, e); this.textureInitializationFailed = true; return null; } } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; LazilyLoadedTexture that = (LazilyLoadedTexture) o; //noinspection RedundantIfStatement if (imageSource != null ? !imageSource.equals(that.imageSource) : that.imageSource != null) return false; return true; } @Override public int hashCode() { return imageSource != null ? imageSource.hashCode() : 0; } }