/*
Copyright (C) 2001, 2006 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.sun.opengl.util.texture.*;
import gov.nasa.worldwind.*;
import gov.nasa.worldwind.avlist.*;
import gov.nasa.worldwind.cache.*;
import gov.nasa.worldwind.geom.*;
import gov.nasa.worldwind.layers.*;
import gov.nasa.worldwind.retrieve.*;
import gov.nasa.worldwind.util.Logging;
import gov.nasa.worldwind.util.*;
import javax.media.opengl.*;
import java.awt.image.*;
import java.io.*;
import java.net.*;
import java.nio.*;
import java.util.logging.Level;
/**
* Renders a single image tile. The image source can be a local file, a <code>BufferedImage</code>
* or a network http source.
* <p>
* Images from remote sources are downloaded in background and will be saved in the cache directory
* </p>
*
* @author Patrick Murris, Tom Gaskins, Antonio Santiago
* @version $Id: SurfaceImage.java 5244 2008-05-01 01:15:59Z patrickmurris $
*/
public class SurfaceImage implements SurfaceTile, Renderable, Movable
{
private static final String DEFAULT_CACHE_DIRECTORY = "SurfaceImages";
private Object imageSource;
private Sector sector;
private Position referencePosition;
private Extent extent;
private double extentVerticalExaggertion = Double.MIN_VALUE; // VE used to calculate the extent
private double opacity = 1.0;
private TextureData textureData = null;
private boolean reload = false; // Force texture data to be reloaded
private boolean useCache = true;
private boolean loading = false; // True when image is loading or downloading
private boolean hasProblem = false; // True when download failed
private Layer layer;
private String cacheDirectory = DEFAULT_CACHE_DIRECTORY;
/**
* Renders a single image tile from a local or remote network source.
*
* @param imageSource can be a local image path, a <code>BufferedImage</code> or a url string pointing to
* an http server.
* @param sector the sector covered by the image.
*/
public SurfaceImage(Object imageSource, Sector sector)
{
initialize(imageSource, sector, null, this.cacheDirectory);
}
/**
* Renders a single image tile from a local or remote network source.
*
* @param imageSource can be a local image path, a <code>BufferedImage</code> or a url string pointing to
* an http server.
* @param sector the sector covered by the image.
* @param layer a reference to the layer handling this image. This layer will fire an event when the image has
* finished downloading.
*/
public SurfaceImage(Object imageSource, Sector sector, Layer layer)
{
initialize(imageSource, sector, layer, this.cacheDirectory);
}
/**
* Renders a single image tile from a local or remote network source.
*
* @param imageSource can be a local image path, a <code>BufferedImage</code> or a url string pointing to
* an http server.
* @param sector the sector covered by the image.
* @param layer a reference to the layer handling this image. This layer will fire an event when the image has
* finished downloading.
* @param cacheDirectory the cache directory where the downloaded image should be saved and retrieved.
*/
public SurfaceImage(Object imageSource, Sector sector, Layer layer, String cacheDirectory)
{
initialize(imageSource, sector, layer, cacheDirectory);
}
private void initialize(Object imageSource, Sector sector, Layer layer, String cacheDirectory)
{
if (imageSource == null)
{
String message = Logging.getMessage("nullValue.ImageSourceIsNull");
Logging.logger().severe(message);
throw new IllegalArgumentException(message);
}
if (sector == null)
{
String message = Logging.getMessage("nullValue.SectorIsNull");
Logging.logger().severe(message);
throw new IllegalArgumentException(message);
}
if (cacheDirectory == null)
{
String message = Logging.getMessage("nullValue.DirectionIsNull");
Logging.logger().severe(message);
throw new IllegalArgumentException(message);
}
this.imageSource = imageSource;
this.sector = sector;
this.referencePosition = new Position(sector.getCentroid(), 0);
this.layer = layer;
this.cacheDirectory = cacheDirectory;
}
/**
* Get the image source object. It can be a <code>String</code> containing a path to either a
* local file or a networked file. It can also be a <code>BufferedImage</code>.
*
* @return the image source object.
*/
public Object getImageSource()
{
return imageSource;
}
public Sector getSector()
{
return this.sector;
}
/**
* Sets the sector for the image allowing to change its size or position.
*
* @param sector the new sector.
*/
public void setSector(Sector sector)
{
if (sector == null)
{
String message = Logging.getMessage("nullValue.SectorIsNull");
Logging.logger().severe(message);
throw new IllegalArgumentException(message);
}
this.sector = sector;
this.extent = null;
}
/**
* Returns if the image is loading texture data.
*
* @return true if the image data is being loaded.
*/
public boolean isLoading()
{
return this.loading;
}
/**
* Returns whether there was any problem loading texture data.
*
* @return true if image data failed to download - or other problems.
*/
public boolean hasProblem()
{
return this.hasProblem;
}
/**
* Force texture data to be reloaded.
*
* @param useCache true if data should be reloaded from the cache.
* @return true if reloading has been succesfully scheduled.
*/
public boolean reload(boolean useCache)
{
if (this.loading)
return false;
this.reload = true;
this.useCache = useCache;
this.loading = false;
this.hasProblem = false;
return true;
}
public Extent getExtent(DrawContext dc)
{
if (dc == null)
{
String msg = Logging.getMessage("nullValue.DrawContextIsNull");
Logging.logger().severe(msg);
throw new IllegalArgumentException(msg);
}
if (this.extent == null || this.extentVerticalExaggertion != dc.getVerticalExaggeration())
{
this.extent = dc.getGlobe().computeBoundingCylinder(dc.getVerticalExaggeration(), this.getSector());
this.extentVerticalExaggertion = dc.getVerticalExaggeration();
}
return this.extent;
}
public double getOpacity()
{
return opacity;
}
public void setOpacity(double opacity)
{
this.opacity = opacity;
}
/**
* Get the layer reference to which this <code>SurfaceImage</code> belongs. May be <code>null</code>
*
* @return the layer reference.
*/
public Layer getLayer()
{
return this.layer;
}
private void setTexture(TextureCache tc, Texture texture)
{
if (tc == null)
{
String message = Logging.getMessage("nullValue.TextureCacheIsNull");
Logging.logger().severe(message);
throw new IllegalStateException(message);
}
tc.put(this.imageSource, texture);
}
private Texture getTexture(TextureCache tc)
{
if (tc == null)
{
String message = Logging.getMessage("nullValue.TextureCacheIsNull");
Logging.logger().severe(message);
throw new IllegalStateException(message);
}
return tc.get(this.imageSource);
}
private Texture initializeTexture(DrawContext dc)
{
if (dc == null)
{
String message = Logging.getMessage("nullValue.DrawContextIsNull");
Logging.logger().severe(message);
throw new IllegalStateException(message);
}
Texture t = null;
if (this.imageSource instanceof String)
{
String path = (String) this.imageSource;
if (path.toLowerCase().startsWith("http"))
{
// Handle remote file
if (this.loading)
return null;
if (this.textureData != null && !this.reload)
{
t = TextureIO.newTexture(this.textureData);
}
else if (!this.hasProblem)
{
sendLoadRequests(path);
return null;
}
}
else
{
// Handle local file or resource
Object streamOrException = WWIO.getFileOrResourceAsStream(path, this.getClass());
if (streamOrException == null || streamOrException instanceof Exception)
{
Logging.logger().log(Level.SEVERE, "layers.TextureLayer.ExceptionAttemptingToReadTextureFile",
streamOrException != null ? streamOrException : "");
return null;
}
try
{
t = TextureIO.newTexture((InputStream) streamOrException, true, null);
}
catch (Exception e)
{
Logging.logger().log(java.util.logging.Level.SEVERE,
"layers.TextureLayer.ExceptionAttemptingToReadTextureFile", e);
return null;
}
}
}
else if (this.imageSource instanceof BufferedImage)
{
try
{
t = TextureIO.newTexture((BufferedImage) this.imageSource, true);
}
catch (Exception e)
{
String msg = Logging.getMessage("generic.IOExceptionDuringTextureInitialization");
Logging.logger().log(Level.SEVERE, msg, e);
return null;
}
}
else
{
Logging.logger().log(java.util.logging.Level.SEVERE, "SurfaceImage.UnknownSourceType",
this.imageSource.getClass().getName());
return null;
}
if (t == null) // In case JOGL TextureIO returned null
{
Logging.logger().log(java.util.logging.Level.SEVERE, "generic.TextureUnreadable",
this.imageSource instanceof String ? this.imageSource : this.imageSource.getClass().getName());
return null;
}
// Textures with the same path are assumed to be identical textures, so key the texture id off the
// image source.
this.setTexture(dc.getTextureCache(), t);
t.bind();
GL gl = dc.getGL();
gl.glTexParameteri(GL.GL_TEXTURE_2D, GL.GL_TEXTURE_MIN_FILTER, GL.GL_LINEAR);//_MIPMAP_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);
return t;
}
public boolean bind(DrawContext dc)
{
if (dc == null)
{
String message = Logging.getMessage("nullValue.DrawContextIsNull");
Logging.logger().severe(message);
throw new IllegalStateException(message);
}
Texture t = this.getTexture(dc.getTextureCache());
if (t == null || this.reload)
{
t = this.initializeTexture(dc);
if (t != null)
return true; // texture was bound during initialization.
}
if (t != null)
t.bind();
return t != null;
}
public void applyInternalTransform(DrawContext dc)
{
if (dc == null)
{
String message = Logging.getMessage("nullValue.DrawContextIsNull");
Logging.logger().severe(message);
throw new IllegalStateException(message);
}
// Use the tile's texture if available.
Texture t = this.getTexture(dc.getTextureCache());
if (t == null)
t = this.initializeTexture(dc);
if (t != null)
{
if (t.getMustFlipVertically())
{
GL gl = GLContext.getCurrent().getGL();
gl.glMatrixMode(GL.GL_TEXTURE);
gl.glLoadIdentity();
gl.glScaled(1, -1, 1);
gl.glTranslated(0, -1, 0);
}
}
}
// Render the surface image tile
public void render(DrawContext dc)
{
if (dc == null)
{
String message = Logging.getMessage("nullValue.DrawContextIsNull");
Logging.logger().severe(message);
throw new IllegalStateException(message);
}
if (!this.sector.intersects(dc.getVisibleSector()))
return;
GL gl = dc.getGL();
try
{
if (!dc.isPickingMode())
{
double opacity = this.layer != null ? this.getOpacity() * this.layer.getOpacity() : this.getOpacity();
if (opacity < 1)
{
gl.glPushAttrib(GL.GL_COLOR_BUFFER_BIT | GL.GL_POLYGON_BIT | GL.GL_CURRENT_BIT);
gl.glColor4d(1d, 1d, 1d, opacity);
}
else
{
gl.glPushAttrib(GL.GL_COLOR_BUFFER_BIT | GL.GL_POLYGON_BIT);
}
gl.glEnable(GL.GL_BLEND);
gl.glBlendFunc(GL.GL_SRC_ALPHA, GL.GL_ONE_MINUS_SRC_ALPHA);
}
else
{
gl.glPushAttrib(GL.GL_POLYGON_BIT);
}
gl.glPolygonMode(GL.GL_FRONT, GL.GL_FILL);
gl.glEnable(GL.GL_CULL_FACE);
gl.glCullFace(GL.GL_BACK);
dc.getGeographicSurfaceTileRenderer().renderTile(dc, this);
}
finally
{
gl.glPopAttrib();
}
}
public boolean equals(Object o)
{
if (this == o)
return true;
if (o == null || this.getClass() != o.getClass())
return false;
SurfaceImage that = (SurfaceImage) o;
return imageSource.equals(that.imageSource) && sector.equals(that.sector);
}
public int hashCode()
{
int result;
result = imageSource.hashCode();
result = 31 * result + sector.hashCode();
return result;
}
private void sendLoadRequests(String path)
{
if (WorldWind.getTaskService().isFull())
return;
this.reload = false;
this.loading = true;
WorldWind.getTaskService().addTask(new RequestTask(path));
}
private class RequestTask implements Runnable
{
private final String path;
public RequestTask(String path)
{
this.path = path;
}
public void run()
{
final java.net.URL textureURL = WorldWind.getDataFileCache().findFile(getCachePath(path), false);
if (textureURL != null && SurfaceImage.this.useCache)
{
// Load cached texture
loadCachedTexture(textureURL);
}
else
{
// Download texture
downloadTexture(path);
}
}
private boolean loadCachedTexture(java.net.URL textureURL)
{
// TODO: handle expiration date/time
/* if (WWIO.isFileOutOfDate(textureURL, 0))
{
// The file has expired. Delete it then request download of newer.
gov.nasa.worldwind.WorldWind.getDataFileCache().removeFile(textureURL);
String message = Logging.getMessage("generic.DataFileExpired", textureURL);
Logging.logger().fine(message);
}
*/
TextureData textureData = null;
try
{
textureData = TextureIO.newTextureData(textureURL, true, null);
SurfaceImage.this.textureData = textureData;
SurfaceImage.this.hasProblem = false;
if (SurfaceImage.this.layer != null)
SurfaceImage.this.layer.firePropertyChange(AVKey.LAYER, null, this);
}
catch (Exception e)
{
Logging.logger().log(java.util.logging.Level.SEVERE,
"layers.TextureLayer.ExceptionAttemptingToReadTextureFile", e);
SurfaceImage.this.hasProblem = true;
}
SurfaceImage.this.loading = false;
return textureData != null;
}
private void downloadTexture(final String path)
{
if (!WorldWind.getRetrievalService().isAvailable())
return;
try
{
URL url = new URL(path);
if ("http".equalsIgnoreCase(url.getProtocol()))
{
// Download asynchronously
if (WorldWind.getNetworkStatus().isHostUnavailable(url))
return;
Retriever retriever = new HTTPRetriever(url, new DownloadPostProcessor());
// Apply any overridden timeouts from the layer.
if (SurfaceImage.this.layer != null)
{
Integer cto = AVListImpl.getIntegerValue(SurfaceImage.this.layer,
AVKey.URL_CONNECT_TIMEOUT);
if (cto != null && cto > 0)
retriever.setConnectTimeout(cto);
Integer cro = AVListImpl.getIntegerValue(SurfaceImage.this.layer,
AVKey.URL_READ_TIMEOUT);
if (cro != null && cro > 0)
retriever.setReadTimeout(cro);
Integer srl = AVListImpl.getIntegerValue(SurfaceImage.this.layer,
AVKey.RETRIEVAL_QUEUE_STALE_REQUEST_LIMIT);
if (srl != null && srl > 0)
retriever.setStaleRequestLimit(srl);
}
WorldWind.getRetrievalService().runRetriever(retriever);
}
else
{
SurfaceImage.this.loading = false;
SurfaceImage.this.hasProblem = true;
Logging.logger().severe(Logging.getMessage("layers.TextureLayer.UnknownRetrievalProtocol", path));
}
}
catch (MalformedURLException ex)
{
SurfaceImage.this.loading = false;
SurfaceImage.this.hasProblem = true;
Logging.logger().severe(Logging.getMessage("layers.TextureLayer.UnknownRetrievalProtocol", path));
}
}
}
private class DownloadPostProcessor implements RetrievalPostProcessor
{
public ByteBuffer run(Retriever retriever)
{
if (retriever == null)
{
// Missing data.
SurfaceImage.this.loading = false;
SurfaceImage.this.hasProblem = true;
String msg = Logging.getMessage("nullValue.RetrieverIsNull");
Logging.logger().severe(msg);
throw new IllegalArgumentException(msg);
}
if (!retriever.getState().equals(Retriever.RETRIEVER_STATE_SUCCESSFUL))
{
// Missing data.
SurfaceImage.this.loading = false;
SurfaceImage.this.hasProblem = true;
return null;
}
HTTPRetriever htr = (HTTPRetriever) retriever;
if (htr.getResponseCode() == HttpURLConnection.HTTP_NO_CONTENT)
{
// Missing data.
SurfaceImage.this.loading = false;
SurfaceImage.this.hasProblem = true;
return null;
}
else if (htr.getResponseCode() != HttpURLConnection.HTTP_OK)
{
// Missing data.
SurfaceImage.this.loading = false;
SurfaceImage.this.hasProblem = true;
return null;
}
URLRetriever r = (URLRetriever) retriever;
ByteBuffer buffer = r.getBuffer();
if (buffer != null)
{
try
{
// Store file in the cache
String name = SurfaceImage.this.getCachePath(htr.getUrl().toString());
final File outFile = WorldWind.getDataFileCache().newFile(name);
if (outFile == null)
{
String msg = Logging.getMessage("generic.CantCreateCacheFile", name);
Logging.logger().warning(msg);
return null;
}
else
{
WWIO.saveBuffer(buffer, outFile);
}
SurfaceImage.this.textureData = TextureIO.newTextureData(
new ByteArrayInputStream(buffer.array()), true, null);
SurfaceImage.this.hasProblem = false;
// Fire layer event
if (SurfaceImage.this.layer != null)
SurfaceImage.this.layer.firePropertyChange(AVKey.LAYER, null, this);
}
catch (Exception e)
{
Logging.logger().log(java.util.logging.Level.SEVERE, Logging.getMessage(
"layers.TextureLayer.ExceptionSavingRetrievedTextureFile", htr.getUrl().toString()), e);
SurfaceImage.this.hasProblem = true;
}
}
SurfaceImage.this.loading = false;
return null;
}
}
private String getCachePath(String path)
{
try
{
URL url = new URL(path);
if (DEFAULT_CACHE_DIRECTORY.equals(this.cacheDirectory))
return this.cacheDirectory + "/" + WWIO.formPath(url.getHost()) + url.getPath();
return this.cacheDirectory + "/" + new File(url.getFile()).getName();
}
catch (MalformedURLException ex)
{
return WWIO.formPath(this.cacheDirectory, path);
}
}
public void move(Position position)
{
if (position == null)
{
String msg = Logging.getMessage("nullValue.PositionIsNull");
Logging.logger().severe(msg);
throw new IllegalArgumentException(msg);
}
// Increase the current sector position.
double minlat = this.sector.getMinLatitude().getDegrees();
double minlon = this.sector.getMinLongitude().getDegrees();
double maxlat = this.sector.getMaxLatitude().getDegrees();
double maxlon = this.sector.getMaxLongitude().getDegrees();
double poslat = position.getLatitude().getDegrees();
double poslon = position.getLongitude().getDegrees();
minlat += poslat;
maxlat += poslat;
minlon += poslon;
maxlon += poslon;
// Check new values don't exceed the limits.
if (maxlat > 90 || maxlat < -90 || minlat > 90 || minlat < -90 ||
maxlon > 180 || maxlon < -180 || minlon > 180 || minlon < -180)
{
return;
}
this.referencePosition.add(position);
setSector(Sector.fromDegrees(minlat, maxlat, minlon, maxlon));
}
public void moveTo(Position position)
{
if (position == null)
{
String msg = Logging.getMessage("nullValue.PositionIsNull");
Logging.logger().severe(msg);
throw new IllegalArgumentException(msg);
}
// Calculate new position
double poslat = position.getLatitude().getDegrees();
double poslon = position.getLongitude().getDegrees();
double halfDeltaLat = this.sector.getDeltaLatDegrees() / 2;
double halfDeltaLon = this.sector.getDeltaLonDegrees() / 2;
double minlat = poslat - halfDeltaLat;
double maxlat = poslat + halfDeltaLat;
double minlon = poslon - halfDeltaLon;
double maxlon = poslon + halfDeltaLon;
// Check new values don't exceed the limits.
if (maxlat > 90 || maxlat < -90 || minlat > 90 || minlat < -90 ||
maxlon > 180 || maxlon < -180 || minlon > 180 || minlon < -180)
{
return;
}
this.referencePosition = position;
setSector(Sector.fromDegrees(minlat, maxlat, minlon, maxlon));
}
public Position getReferencePosition()
{
return this.referencePosition;
}
}