/**
   * Causes resources used by the World Window to be freed. The World Window cannot be used once
   * this method is called. An OpenGL context for the window must be current.
   */
  public void shutdown() {
    WorldWind.getDataFileStore().removePropertyChangeListener(this);

    if (this.inputHandler != null) {
      this.inputHandler.dispose();
      this.inputHandler = new NoOpInputHandler();
    }

    // Clear the texture cache
    if (this.getGpuResourceCache() != null) this.getGpuResourceCache().clear();

    // Dispose all the layers //  TODO: Need per-window dispose for layers
    if (this.getModel() != null && this.getModel().getLayers() != null) {
      for (Layer layer : this.getModel().getLayers()) {
        try {
          layer.dispose();
        } catch (Exception e) {
          Logging.logger()
              .log(
                  java.util.logging.Level.SEVERE,
                  Logging.getMessage("WorldWindowGLCanvas.ExceptionWhileShuttingDownWorldWindow"),
                  e);
        }
      }
    }

    SceneController sc = this.getSceneController();
    if (sc != null) sc.dispose();
  }
    public ByteBuffer run(Retriever retriever) {
      if (!retriever.getState().equals(Retriever.RETRIEVER_STATE_SUCCESSFUL)) return null;

      HTTPRetriever htr = (HTTPRetriever) retriever;
      if (htr.getResponseCode() == HttpURLConnection.HTTP_NO_CONTENT) {
        // Mark tile as missing to avoid excessive attempts
        MercatorTiledImageLayer.this.levels.markResourceAbsent(tile);
        return null;
      }

      if (htr.getResponseCode() != HttpURLConnection.HTTP_OK) return null;

      URLRetriever r = (URLRetriever) retriever;
      ByteBuffer buffer = r.getBuffer();

      String suffix = WWIO.makeSuffixForMimeType(htr.getContentType());
      if (suffix == null) {
        return null; // TODO: log error
      }

      String path = tile.getPath().substring(0, tile.getPath().lastIndexOf("."));
      path += suffix;

      final File outFile = WorldWind.getDataFileStore().newFile(path);
      if (outFile == null) return null;

      try {
        WWIO.saveBuffer(buffer, outFile);
        return buffer;
      } catch (IOException e) {
        e.printStackTrace(); // TODO: log error
        return null;
      }
    }
  public WorldWindowImpl() {
    this.sceneController =
        (SceneController) WorldWind.createConfigurationComponent(AVKey.SCENE_CONTROLLER_CLASS_NAME);

    // Set up to initiate a repaint whenever a file is retrieved and added to the local file store.
    WorldWind.getDataFileStore().addPropertyChangeListener(this);
  }
  /**
   * Resolves a reference to a remote element identified by address and identifier, where {@code
   * linkBase} identifies a remote document, and {@code linkRef} is the id of the desired element.
   * This method retrieves resources asynchronously using the {@link
   * gov.nasa.worldwind.cache.FileStore}.
   *
   * <p>The return value is null if the file is not yet available in the FileStore. If {@code
   * linkBase} refers to a COLLADA file and {@code linkRef} is non-null, the return value is the
   * element identified by {@code linkRef}. If {@code linkBase} refers to a COLLADA file and {@code
   * linkRef} is null, the return value is a parsed {@link ColladaRoot} for the COLLADA file
   * identified by {@code linkBase}. Otherwise the return value is a {@link URL} to the file in the
   * file cache.
   *
   * @param linkBase the address of the document containing the requested element.
   * @param linkRef the element's identifier.
   * @return URL to the requested file, parsed ColladaRoot, or COLLADA element. Returns null if the
   *     document is not yet available in the FileStore.
   * @throws IllegalArgumentException if the {@code linkBase} is null.
   */
  public Object resolveRemoteReference(String linkBase, String linkRef) {
    if (linkBase == null) {
      String message = Logging.getMessage("nullValue.DocumentSourceIsNull");
      Logging.logger().severe(message);
      throw new IllegalArgumentException(message);
    }

    try {
      // See if it's in the cache. If not, requestFile will start another thread to retrieve it and
      // return null.
      URL url = WorldWind.getDataFileStore().requestFile(linkBase);
      if (url == null) return null;

      // It's in the cache. If it's a COLLADA file try to parse it so we can search for the
      // specified reference.
      // If it's not COLLADA, just return the url for the cached file.
      String contentType = WorldWind.getDataFileStore().getContentType(linkBase);
      if (contentType == null) {
        String suffix = WWIO.getSuffix(linkBase.split(";")[0]); // strip of trailing garbage
        if (!WWUtil.isEmpty(suffix)) contentType = WWIO.makeMimeTypeForSuffix(suffix);
      }

      if (!this.canParseContentType(contentType)) return url;

      // If the file is a COLLADA document, attempt to open it. We can't open it as a File with
      // createAndParse
      // because the ColladaRoot that will be created needs to have the remote address in order to
      // resolve any
      // relative references within it.
      ColladaRoot refRoot = this.parseCachedColladaFile(url, linkBase);

      // Add the parsed file to the session cache so it doesn't have to be parsed again.
      WorldWind.getSessionCache().put(linkBase, refRoot);

      // Now check the newly opened COLLADA file for the referenced item, if a reference was
      // specified.
      if (linkRef != null) return refRoot.getItemByID(linkRef);
      else return refRoot;
    } catch (Exception e) {
      String message =
          Logging.getMessage("generic.UnableToResolveReference", linkBase + "/" + linkRef);
      Logging.logger().warning(message);
      return null;
    }
  }
  /*
   * Load cache locations
   */
  private void loadData() {
    // load combo with WW caches
    // List<File> caches =  WorldWind.getDataFileCache().getCacheLocations();
    List<? extends File> caches = WorldWind.getDataFileStore().getLocations();

    for (File file : caches) {
      combo.add(file.toString());
    }

    // load table w/ 1st location files
    // loadTable(caches.get(0));
  }
  @Override
  protected boolean handleElevations(
      Globe globe, TextureTile tile, Sector sector, BufferWrapper[] elevations) {
    int width = tile.getLevel().getTileWidth();
    int height = tile.getLevel().getTileHeight();

    double[] minmax = getMinMax(elevations[4], elevationModel.getMissingDataSignal());
    byte[][] bytes =
        elevationsToTexture(width, height, globe, sector, elevations, minmax[0], minmax[1]);

    File file = WorldWind.getDataFileStore().newFile(tile.getPath());
    return saveTexture(file, bytes, width, height, minmax[0], minmax[1]);
  }
  private BufferedImage requestImage(MercatorTextureTile tile, String mimeType)
      throws URISyntaxException {
    String pathBase = tile.getPath().substring(0, tile.getPath().lastIndexOf("."));
    String suffix = WWIO.makeSuffixForMimeType(mimeType);
    String path = pathBase + suffix;
    URL url = WorldWind.getDataFileStore().findFile(path, false);

    if (url == null) // image is not local
    return null;

    if (WWIO.isFileOutOfDate(url, tile.getLevel().getExpiryTime())) {
      // The file has expired. Delete it.
      WorldWind.getDataFileStore().removeFile(url);
      String message = Logging.getMessage("generic.DataFileExpired", url);
      Logging.logger().fine(message);
    } else {
      try {
        File imageFile = new File(url.toURI());
        BufferedImage image = ImageIO.read(imageFile);
        if (image == null) {
          String message = Logging.getMessage("generic.ImageReadFailed", imageFile);
          throw new RuntimeException(message);
        }

        this.levels.unmarkResourceAbsent(tile);
        return image;
      } catch (IOException e) {
        // Assume that something's wrong with the file and delete it.
        gov.nasa.worldwind.WorldWind.getDataFileStore().removeFile(url);
        this.levels.markResourceAbsent(tile);
        String message = Logging.getMessage("generic.DeletedCorruptDataFile", url);
        Logging.logger().info(message);
      }
    }

    return null;
  }
/**
 * Represents a texture derived from a lazily loaded image source such as an image file or a {@link
 * java.awt.image.BufferedImage}.
 *
 * <p>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.
 *
 * <p>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
 * <code>bind</code> or <code>applyInternalTransform</code> 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$
 */
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 <code>volatile</code> in order to synchronize atomic
   * access among threads. This field is not used if the image source is <code>BufferedImage</code>.
   */
  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 <code>imageSource</code> 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 <code>imageSource</code> 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 <code>BufferedImage</code>
   *
   * @return true if the image source is a <code>BufferedImage</code>, 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 <code>
   * TextureData</code>. 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 <code>
   * TextureData</code>. 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}
   *
   * <p>This method behaves identically to {@link #getWidth()}. The <code>DrawContext</code>
   * 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}
   *
   * <p>This method behaves identically to {@link #getHeight()}. The <code>DrawContext</code>
   * 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 <code>Texture</code> has not
   * yet been created for it.
   *
   * <p>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.
   *
   * <p>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 <code>GLContext</code> 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();
      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()) {
      GL gl = GLContext.getCurrent().getGL();
      gl.glMatrixMode(GL.GL_TEXTURE);
      gl.glLoadIdentity();
      gl.glScaled(1, -1, 1);
      gl.glTranslated(0, -1, 0);
    }
  }

  /**
   * If this instance's image source is a <code>BufferedImage</code>, 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 <code>run</code> 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 <code>BufferedImage<code>.
   *
   * @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 <code>BufferedImage</code>.
   */
  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 =
          TextureIO.newTextureData((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 <code>TextureData
   * </code> exists.
   *
   * @param dc the current draw context.
   * @return the newly created texture, or null if this instance has no current <code>TextureData
   *     </code> 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 <code>Texture</code> 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 <code>TextureData</code> instance for the image.
   */
  protected TextureData readImage(URL fileUrl) {
    try {
      return TextureIO.newTextureData(fileUrl, this.isUseMipMaps(), null);
    } 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;
  }
}
  @Override
  protected boolean loadTexture(TextureTile tile, URL textureURL) {
    if (!(tile instanceof MinMaxTextureTile)) {
      Logging.logger().severe("Tile is not instance of " + MinMaxTextureTile.class);
      getLevels().markResourceAbsent(tile);
      return false;
    }

    synchronized (fileLock) {
      InputStream is = null;
      try {
        is = textureURL.openStream();
        DataInputStream dis = new DataInputStream(is);

        int width = dis.readInt();
        int height = dis.readInt();
        int bands = dis.readInt();
        double minElevation = dis.readDouble();
        double maxElevation = dis.readDouble();
        byte[][] bytes = new byte[bands][];
        for (int i = 0; i < bands; i++) {
          bytes[i] = new byte[width * height];
          is.read(bytes[i]);
        }

        DataBuffer db = new DataBufferByte(bytes, width * height);
        SampleModel sm = new BandedSampleModel(DataBuffer.TYPE_BYTE, width, height, bands);
        Raster raster = Raster.createRaster(sm, db, null);
        BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB_PRE);
        image.setData(raster);

        TextureData textureData = TextureIO.newTextureData(image, isUseMipMaps());
        if (textureData == null) {
          throw new Exception("Could not create texture data for " + textureURL);
        }

        ((MinMaxTextureTile) tile).setMinElevation(minElevation);
        ((MinMaxTextureTile) tile).setMaxElevation(maxElevation);
        tile.setTextureData(textureData);

        // if (tile.getLevelNumber() != 0 || !this.isRetainLevelZeroTiles())
        addTileToCache(tile);

        getLevels().unmarkResourceAbsent(tile);
        firePropertyChange(AVKey.LAYER, null, this);
        return true;
      } catch (Exception e) {
        // Assume that something's wrong with the file and delete it.
        gov.nasa.worldwind.WorldWind.getDataFileStore().removeFile(textureURL);
        getLevels().markResourceAbsent(tile);
        String message = Logging.getMessage("generic.DeletedCorruptDataFile", textureURL);
        Logging.logger().info(message + ":" + e.getLocalizedMessage());
        return false;
      } finally {
        if (is != null) {
          try {
            is.close();
          } catch (IOException e) {
          }
        }
      }
    }
  }