src.gov.nasa.worldwind.render.ScreenImage Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of worldwindx Show documentation
Show all versions of worldwindx Show documentation
World Wind is a collection of components that interactively display 3D geographic information within Java applications or applets.
/*
* 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.TextureCoords;
import gov.nasa.worldwind.*;
import gov.nasa.worldwind.avlist.AVKey;
import gov.nasa.worldwind.layers.Layer;
import gov.nasa.worldwind.ogc.kml.KMLConstants;
import gov.nasa.worldwind.ogc.kml.impl.KMLExportUtil;
import gov.nasa.worldwind.pick.PickSupport;
import gov.nasa.worldwind.util.*;
import javax.media.opengl.*;
import javax.xml.stream.*;
import java.awt.*;
import java.io.*;
import java.net.URL;
/**
* Draws an image parallel to the screen at a specified screen location relative to the World Window. If no image is
* specified, a filled rectangle is drawn in its place.
*
* @author tag
* @version $Id: ScreenImage.java 1171 2013-02-11 21:45:02Z dcollins $
*/
public class ScreenImage extends WWObjectImpl implements Renderable, Exportable
{
protected Object imageSource;
protected BasicWWTexture texture;
protected OrderedImage orderedImage = new OrderedImage();
protected PickSupport pickSupport = new PickSupport();
protected double opacity = 1d;
protected Double rotation;
protected Color color = Color.WHITE;
protected Object delegateOwner;
protected Size size = new Size();
protected Offset screenOffset;
protected Offset imageOffset;
protected Offset rotationOffset;
// Values computed once per frame and reused during the frame as needed.
protected long frameNumber = -1; // Identifies frame used to calculate these values
protected int width; // Width of scaled image
protected int height; // Height of scaled image
protected int originalImageWidth; // Width of unscaled image
protected int originalImageHeight; // Height of unscaled image
protected Point rotationPoint;
/**
* Indicates the location of this screen image in the viewport (on the screen) in OpenGL coordinates. This property
* is computed in computeOffsets
and used in draw
Initially null
.
*/
protected Point screenLocation;
/**
* Indicates the location of this screen image in the viewport (on the screen) in AWT coordinates. This property is
* assigned in setScreenLocation
and computeOffsets
. In computeOffsets
, this
* is computed by converting the screenLocation
from OpenGL coordinates to AWT coordinates. Initially
* null
.
*/
protected Point awtScreenLocation;
protected double dx;
protected double dy;
protected Layer pickLayer;
protected class OrderedImage implements OrderedRenderable
{
public double getDistanceFromEye()
{
return 0;
}
public void pick(DrawContext dc, Point pickPoint)
{
ScreenImage.this.draw(dc);
}
public void render(DrawContext dc)
{
ScreenImage.this.draw(dc);
}
}
/**
* Returns the location of the image on the screen. The position is relative to the upper-left corner of the World
* Window. The point specified by the image offset will be aligned to this point. If the position was specified as
* an offset, it may change if the viewport size changes. The value returned by this method is the most recently
* computed screen location. Call {@link #getScreenLocation(DrawContext)} to ensure an accurate result based on the
* current viewport.
*
* @return the current screen position.
*
* @see #getScreenLocation(DrawContext)
* @see #getImageOffset()
* @see #getScreenOffset()
*/
public Point getScreenLocation()
{
return this.awtScreenLocation;
}
/**
* Returns the location of the image on the screen. The position is relative to the upper-left corner of the World
* Window. The image is centered on this position.
*
* @param dc The DrawContext in which the image will be drawn.
*
* @return the current screen position.
*/
public Point getScreenLocation(DrawContext dc)
{
this.computeOffsets(dc);
return this.awtScreenLocation;
}
/**
* Convenience method to specify the location of the image on the screen. The specified screenLocation
* is relative to the upper-left corner of the World Window, and the image is centered on this location.
*
* @param screenLocation the screen location on which to center the image. May be null, in which case the image is
* not displayed.
*
* @see #setScreenOffset(Offset)
* @see #setImageOffset(Offset)
*/
public void setScreenLocation(Point screenLocation)
{
// Use units PIXELS for the X screen offset, and and INSET_PIXELS for the Y screen offset. The Offset is in
// OpenGL coordinates with the origin in the lower-left corner, but the Point is in AWT coordinates with the
// origin in the upper-left corner. This offset translates the origin from the lower-left to the upper-left
// corner.
this.screenOffset = new Offset(screenLocation.getX(), screenLocation.getY(), AVKey.PIXELS, AVKey.INSET_PIXELS);
this.imageOffset = new Offset(0.5, 0.5, AVKey.FRACTION, AVKey.FRACTION);
// Set cached screen location to the initial screen location so that it can be retrieved if getScreenLocation()
// is called before the image is rendered. This maintains backward compatibility with the previous behavior of
// ScreenImage.
this.awtScreenLocation = new Point(screenLocation);
}
/**
* Get the offset of the point on the screen to align with the image offset point.
*
* @return Offset of the image point that will be aligned to the image offset point.
*
* @see #getImageOffset()
*/
public Offset getScreenOffset()
{
return screenOffset;
}
/**
* Set the offset of the image relative to the viewport. The screen point identified by this offset will be aligned
* to the image point identified by the image offset.
*
* @param screenOffset The screen offset.
*
* @see #setImageOffset(Offset)
*/
public void setScreenOffset(Offset screenOffset)
{
this.screenOffset = screenOffset;
}
/**
* Get the offset of the point on the image to align with the screen offset point.
*
* @return Offset of the image point that will be aligned to the screen offset point.
*
* @see #getScreenOffset()
*/
public Offset getImageOffset()
{
return imageOffset;
}
/**
* Set the image offset point. This point will be aligned to the screen point identified by the screen offset.
*
* @param imageOffset Offset that identifies a point on the image to align with the screen offset point.
*
* @see #setScreenOffset(Offset)
*/
public void setImageOffset(Offset imageOffset)
{
this.imageOffset = imageOffset;
}
/**
* Get the rotation applied to the image.
*
* @return Rotation in decimal degrees, or null if there is no rotation.
*
* @see #getRotationOffset()
*/
public Double getRotation()
{
return rotation;
}
/**
* Specifies a rotation to be applied to the image.
*
* @param rotation Rotation in decimal degrees.
*
* @see #setRotationOffset(Offset)
*/
public void setRotation(Double rotation)
{
this.rotation = rotation;
}
/**
* Get the point about which the image is rotated.
*
* @return Rotation point in image coordinates, or null if there is no rotation point set. The origin of the
* coordinate system is at the lower left corner of the image.
*
* @see #getRotation()
*/
public Offset getRotationOffset()
{
return rotationOffset;
}
/**
* Set the point on the image about which rotation is performed.
*
* @param rotationOffset Rotation offset.
*
* @see #setRotation(Double)
*/
public void setRotationOffset(Offset rotationOffset)
{
this.rotationOffset = rotationOffset;
}
/**
* Get the dimension to apply to the image.
*
* @return Image dimension.
*/
public Size getSize()
{
return size;
}
/**
* Set a dynamic dimension to apply to the image. The dimension allows the image to be scaled relative to the
* viewport size.
*
* @param size Image dimension. May not be null.
*/
public void setSize(Size size)
{
if (size == null)
{
String msg = Logging.getMessage("nullValue.SizeIsNull");
Logging.logger().severe(msg);
throw new IllegalArgumentException(msg);
}
this.size = size;
}
/**
* Returns the current image source.
*
* @return the current image source.
*
* @see #getImageSource()
*/
public Object getImageSource()
{
return this.imageSource;
}
/**
* Specifies the image source, which may be either a file path {@link String} or a {@link
* java.awt.image.BufferedImage}. If the image is not already in memory, it will be loaded in the background.
*
* @param imageSource the image source, either a file path {@link String} or a {@link
* java.awt.image.BufferedImage}.
*
* @throws IllegalArgumentException if the imageSource
is null.
*/
public void setImageSource(Object imageSource)
{
if (imageSource == null)
{
String message = Logging.getMessage("nullValue.ImageSource");
Logging.logger().severe(message);
throw new IllegalArgumentException(message);
}
this.imageSource = imageSource;
this.texture = null; // New image source, we need to load a new texture
}
/**
* Create and initialize the texture from the image source. If the image is not in memory this method will request
* that it be loaded and return null.
*
* @return The texture, or null if the texture is not yet available.
*/
protected BasicWWTexture initializeTexture()
{
Object imageSource = this.getImageSource();
if (imageSource instanceof String || imageSource instanceof URL)
{
URL imageURL = WorldWind.getDataFileStore().requestFile(imageSource.toString());
if (imageURL != null)
{
this.texture = new BasicWWTexture(imageURL, true);
this.texture.setUseAnisotropy(false);
}
}
else if (imageSource != null)
{
this.texture = new BasicWWTexture(imageSource, true);
return this.texture;
}
return null;
}
/**
* Returns the opacity of the surface. A value of 1 or greater means the surface is fully opaque, a value of 0 means
* that the surface is fully transparent.
*
* @return the surface opacity.
*/
public double getOpacity()
{
return opacity;
}
/**
* If no image is set, or if the image is not yet available, a rectangle will be drawn in this color.
*
* @return The color for the default rectangle.
*/
public Color getColor()
{
return this.color;
}
/**
* Set the color of the rectangle drawn when the image cannot be drawn. The image may not be drawn because it has
* not been loaded, or because no image has been set.
*
* @param defaultColor New color for the default rectangle.
*/
public void setColor(Color defaultColor)
{
this.color = defaultColor;
}
/**
* Sets the opacity of the surface. A value of 1 or greater means the surface is fully opaque, a value of 0 means
* that the surface is fully transparent.
*
* @param opacity a positive value indicating the opacity of the surface.
*
* @throws IllegalArgumentException if the specified opacity is less than zero.
*/
public void setOpacity(double opacity)
{
if (opacity < 0)
{
String message = Logging.getMessage("generic.OpacityOutOfRange", opacity);
Logging.logger().severe(message);
throw new IllegalArgumentException(message);
}
this.opacity = opacity;
}
/**
* Returns the width of the source image after dynamic scaling has been applied. If no image has been specified, but
* a dimension has been specified, the width of the dimension is returned.
*
* @param dc the current draw context.
*
* @return the source image width after scaling.
*
* @see #getSize()
*/
public int getImageWidth(DrawContext dc)
{
if (dc == null)
{
String msg = Logging.getMessage("nullValue.DrawContextIsNull");
Logging.logger().severe(msg);
throw new IllegalArgumentException(msg);
}
this.computeOffsets(dc);
return this.width;
}
/**
* Returns the height of the image after dynamic scaling has been applied. If no image has been specified, but a
* dimension has been specified, the height of the dimension is returned.
*
* @param dc the current draw context.
*
* @return the source image height after scaling.
*
* @see #getSize()
*/
public int getImageHeight(DrawContext dc)
{
if (dc == null)
{
String msg = Logging.getMessage("nullValue.DrawContextIsNull");
Logging.logger().severe(msg);
throw new IllegalArgumentException(msg);
}
this.computeOffsets(dc);
return this.height;
}
/**
* Indicates the object included in {@link gov.nasa.worldwind.event.SelectEvent}s when this object is picked.
*
* @return the object identified as the picked object.
*/
public Object getDelegateOwner()
{
return delegateOwner;
}
/**
* Specify the object to identify as the picked object when this shape is picked.
*
* @param delegateOwner the object included in {@link gov.nasa.worldwind.event.SelectEvent}s as the picked object.
*/
public void setDelegateOwner(Object delegateOwner)
{
this.delegateOwner = delegateOwner;
}
/**
* Compute the image size, rotation, and position based on the current viewport size. This method updates the
* calculated values for screen point, rotation point, width, and height. The calculation is not performed if the
* values have already been calculated for this frame.
*
* @param dc DrawContext into which the image will be rendered.
*/
protected void computeOffsets(DrawContext dc)
{
if (dc.getFrameTimeStamp() != this.frameNumber)
{
final BasicWWTexture texture = this.getTexture();
final int viewportWidth = dc.getView().getViewport().width;
final int viewportHeight = dc.getView().getViewport().height;
// Compute image size
if (texture != null)
{
this.originalImageWidth = texture.getWidth(dc);
this.originalImageHeight = texture.getHeight(dc);
}
else if (this.getImageSource() == null) // If no image source is set, draw a rectangle
{
this.originalImageWidth = 1;
this.originalImageHeight = 1;
}
else // If an image source is set, but the image is not available yet, don't draw anything
{
this.frameNumber = dc.getFrameTimeStamp();
return;
}
if (this.size != null)
{
Dimension d = this.size.compute(this.originalImageWidth, this.originalImageHeight,
viewportWidth, viewportHeight);
this.width = d.width;
this.height = d.height;
}
else
{
this.width = this.originalImageWidth;
this.height = this.originalImageHeight;
}
// Compute rotation
Offset rotationOffset = this.getRotationOffset();
// If no rotation offset is set, rotate around the center of the image.
if (rotationOffset != null)
{
// The KML specification according to both OGC and Google states that the rotation point is specified in
// a coordinate system with the origin at the lower left corner of the screen (0.5, 0.5 is the center
// of the screen). But Google Earth interprets the point in a coordinate system with origin at the lower
// left corner of the image (0.5, 0.5 is the center of the image), so we'll do that too.
Point.Double pointD = rotationOffset.computeOffset(this.width, this.height, null, null);
rotationPoint = new Point((int) pointD.x, (int) pointD.y);
}
else
{
this.rotationPoint = new Point(this.width, this.height);
}
// Compute position
if (this.screenOffset != null)
{
// Compute the screen location in OpenGL coordinates. There is no need to convert from AWT to OpenGL
// coordinates because the Offset is already in OpenGL coordinates with its origin in the lower-left
// corner.
Point.Double pointD = this.screenOffset.computeOffset(viewportWidth, viewportHeight, null, null);
this.screenLocation = new Point((int) pointD.x, (int) (pointD.y));
}
else
{
this.screenLocation = new Point(viewportWidth / 2, viewportHeight / 2);
}
// Convert the screen location from OpenGL to AWT coordinates and store the result in awtScreenLocation. The
// awtScreenLocation property is used in getScreenLocation to indicate the screen location in AWT
// coordinates.
this.awtScreenLocation = new Point(this.screenLocation.x, viewportHeight - this.screenLocation.y);
Point.Double overlayPoint;
if (this.imageOffset != null)
overlayPoint = this.imageOffset.computeOffset(this.width, this.height, null, null);
else
overlayPoint = new Point.Double(this.originalImageWidth / 2.0, this.originalImageHeight / 2.0);
this.dx = -overlayPoint.x;
this.dy = -overlayPoint.y;
this.frameNumber = dc.getFrameTimeStamp();
}
}
/**
* Get the texture for this image. The texture is loaded on a background thread. This method will return null until
* the texture has been loaded.
*
* @return The texture or null if the texture is not yet available.
*/
protected BasicWWTexture getTexture()
{
if (this.texture != null)
return this.texture;
else
return this.initializeTexture();
}
public void render(DrawContext dc)
{
this.computeOffsets(dc);
this.doRender(dc);
}
@SuppressWarnings({"UnusedParameters"})
public void pick(DrawContext dc, Point pickPoint)
{
this.doRender(dc);
}
protected void doRender(DrawContext dc)
{
if (dc.isPickingMode())
this.pickLayer = dc.getCurrentLayer();
dc.addOrderedRenderable(this.orderedImage);
}
protected void draw(DrawContext dc)
{
if (this.screenLocation == null)
return;
GL2 gl = dc.getGL().getGL2(); // GL initialization checks for GL2 compatibility.
boolean attribsPushed = false;
boolean modelviewPushed = false;
boolean projectionPushed = false;
try
{
gl.glPushAttrib(GL2.GL_DEPTH_BUFFER_BIT
| GL2.GL_COLOR_BUFFER_BIT
| GL2.GL_ENABLE_BIT
| GL2.GL_TRANSFORM_BIT
| GL2.GL_VIEWPORT_BIT
| GL2.GL_CURRENT_BIT);
attribsPushed = true;
// Don't depth buffer.
gl.glDisable(GL.GL_DEPTH_TEST);
// Suppress any fully transparent image pixels
gl.glEnable(GL2.GL_ALPHA_TEST);
gl.glAlphaFunc(GL2.GL_GREATER, 0.001f);
java.awt.Rectangle viewport = dc.getView().getViewport();
gl.glMatrixMode(GL2.GL_PROJECTION);
gl.glPushMatrix();
projectionPushed = true;
gl.glLoadIdentity();
gl.glOrtho(0d, viewport.width, 0d, viewport.height, -1, 1);
gl.glMatrixMode(GL2.GL_MODELVIEW);
gl.glPushMatrix();
modelviewPushed = true;
gl.glLoadIdentity();
// Apply the screen location transform. The screen location is in OpenGL coordinates with the origin in the
// lower-left corner, so there is no need to translate from AWT to OpenGL coordinates here.
gl.glTranslated(this.screenLocation.x + this.dx, this.screenLocation.y + this.dy, 0d);
Double rotation = this.getRotation();
if (rotation != null)
{
gl.glTranslated(rotationPoint.x, rotationPoint.y, 0);
gl.glRotated(rotation, 0, 0, 1);
gl.glTranslated(-rotationPoint.x, -rotationPoint.y, 0);
}
double xscale = (double) this.getImageWidth(dc) / originalImageWidth;
double yscale = (double) this.getImageHeight(dc) / originalImageHeight;
if (!dc.isPickingMode())
{
// Draw either an image or a filled rectangle
boolean drawImage = this.getTexture() != null;
gl.glEnable(GL.GL_TEXTURE_2D);
if (drawImage)
{
if (this.getTexture().bind(dc))
gl.glColor4d(1d, 1d, 1d, this.opacity);
else
drawImage = false; // Can't bind texture, draw rectangle instead
}
gl.glEnable(GL.GL_BLEND);
gl.glBlendFunc(GL.GL_SRC_ALPHA, GL.GL_ONE_MINUS_SRC_ALPHA);
if (drawImage)
{
TextureCoords texCoords = this.getTexture().getTexCoords();
gl.glScaled(xscale * this.originalImageWidth, yscale * this.originalImageHeight, 1d);
dc.drawUnitQuad(texCoords);
gl.glBindTexture(GL.GL_TEXTURE_2D, 0);
}
else
{
// Set color of the rectangle that will be drawn instead of an image
final Color color = this.getColor();
float[] colorRGB = color.getRGBColorComponents(null);
gl.glColor4d(colorRGB[0], colorRGB[1], colorRGB[2], (double) color.getAlpha() / 255);
// Don't have texture, just draw a rectangle
gl.glScaled(xscale, yscale, 1d);
dc.drawUnitQuad();
}
}
else
{
this.pickSupport.clearPickList();
this.pickSupport.beginPicking(dc);
Color color = dc.getUniquePickColor();
int colorCode = color.getRGB();
this.pickSupport.addPickableObject(colorCode, this.delegateOwner != null ? this.delegateOwner : this,
null, false);
gl.glColor3ub((byte) color.getRed(), (byte) color.getGreen(), (byte) color.getBlue());
gl.glScaled(xscale * this.originalImageWidth, yscale * this.originalImageHeight, 1d);
dc.drawUnitQuad();
this.pickSupport.endPicking(dc);
this.pickSupport.resolvePick(dc, dc.getPickPoint(), this.pickLayer);
}
}
finally
{
if (projectionPushed)
{
gl.glMatrixMode(GL2.GL_PROJECTION);
gl.glPopMatrix();
}
if (modelviewPushed)
{
gl.glMatrixMode(GL2.GL_MODELVIEW);
gl.glPopMatrix();
}
if (attribsPushed)
gl.glPopAttrib();
}
}
/** {@inheritDoc} */
public String isExportFormatSupported(String format)
{
if (KMLConstants.KML_MIME_TYPE.equalsIgnoreCase(format))
return Exportable.FORMAT_SUPPORTED;
else
return Exportable.FORMAT_NOT_SUPPORTED;
}
/**
* Export the screen image. The {@code output} object will receive the exported data. The type of this object
* depends on the export format. The formats and object types supported by this class are:
*
*
* Format Supported output object types
* ================================================================================
* KML (application/vnd.google-earth.kml+xml) java.io.Writer
* java.io.OutputStream
* javax.xml.stream.XMLStreamWriter
*
*
* @param mimeType MIME type of desired export format.
* @param output An object that will receive the exported data. The type of this object depends on the export
* format (see above).
*
* @throws java.io.IOException If an exception occurs writing to the output object.
*/
public void export(String mimeType, Object output) throws IOException
{
if (mimeType == null)
{
String message = Logging.getMessage("nullValue.Format");
Logging.logger().severe(message);
throw new IllegalArgumentException(message);
}
if (output == null)
{
String message = Logging.getMessage("nullValue.OutputBufferIsNull");
Logging.logger().severe(message);
throw new IllegalArgumentException(message);
}
if (KMLConstants.KML_MIME_TYPE.equalsIgnoreCase(mimeType))
{
try
{
exportAsKML(output);
}
catch (XMLStreamException e)
{
Logging.logger().throwing(getClass().getName(), "export", e);
throw new IOException(e);
}
}
else
{
String message = Logging.getMessage("Export.UnsupportedFormat", mimeType);
Logging.logger().warning(message);
throw new UnsupportedOperationException(message);
}
}
/**
* Export the screen image to KML as a {@code } element. The {@code output} object will receive the
* data. This object must be one of: java.io.Writer java.io.OutputStream javax.xml.stream.XMLStreamWriter.
*
* The image path can only be exported if the image source is a path or URL. If the image source is a BufferedImage,
* for example, the image will not be exported and no icon reference will be written into the ScreenOverlay tag.
*
* @param output Object to receive the generated KML.
*
* @throws XMLStreamException If an exception occurs while writing the KML
* @throws IOException if an exception occurs while exporting the data.
* @see #export(String, Object)
*/
protected void exportAsKML(Object output) throws IOException, XMLStreamException
{
XMLStreamWriter xmlWriter = null;
XMLOutputFactory factory = XMLOutputFactory.newInstance();
boolean closeWriterWhenFinished = true;
if (output instanceof XMLStreamWriter)
{
xmlWriter = (XMLStreamWriter) output;
closeWriterWhenFinished = false;
}
else if (output instanceof Writer)
{
xmlWriter = factory.createXMLStreamWriter((Writer) output);
}
else if (output instanceof OutputStream)
{
xmlWriter = factory.createXMLStreamWriter((OutputStream) output);
}
if (xmlWriter == null)
{
String message = Logging.getMessage("Export.UnsupportedOutputObject");
Logging.logger().warning(message);
throw new IllegalArgumentException(message);
}
xmlWriter.writeStartElement("ScreenOverlay");
xmlWriter.writeStartElement("visibility");
xmlWriter.writeCharacters("1");
xmlWriter.writeEndElement();
String imgSrcString = null;
Object imageSource = this.getImageSource();
if (imageSource instanceof String)
imgSrcString = (String) imageSource;
else if (imageSource instanceof URL)
imgSrcString = imageSource.toString();
// We can only export a link to the image if the image source is a path or URL.
if (imgSrcString != null)
{
xmlWriter.writeStartElement("Icon");
xmlWriter.writeStartElement("href");
xmlWriter.writeCharacters(imgSrcString);
xmlWriter.writeEndElement(); // href
xmlWriter.writeEndElement(); // Icon
}
else
{
// No image string, try to export the color
Color color = this.getColor();
if (color != null)
{
xmlWriter.writeStartElement("color");
xmlWriter.writeCharacters(KMLExportUtil.stripHexPrefix(WWUtil.encodeColorABGR(color)));
xmlWriter.writeEndElement();
}
}
KMLExportUtil.exportOffset(xmlWriter, this.getImageOffset(), "overlayXY");
KMLExportUtil.exportOffset(xmlWriter, this.getScreenOffset(), "screenXY");
Double rotation = this.getRotation();
if (rotation != null)
{
xmlWriter.writeStartElement("rotation");
xmlWriter.writeCharacters(rotation.toString());
xmlWriter.writeEndElement(); // rotation
}
KMLExportUtil.exportOffset(xmlWriter, this.getRotationOffset(), "rotationXY");
KMLExportUtil.exportDimension(xmlWriter, this.getSize(), "size");
xmlWriter.writeEndElement(); // ScreenOverlay
xmlWriter.flush();
if (closeWriterWhenFinished)
xmlWriter.close();
}
}