src.gov.nasa.worldwind.render.SurfaceIcon 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.Movable;
import gov.nasa.worldwind.geom.*;
import gov.nasa.worldwind.globes.Globe;
import gov.nasa.worldwind.util.*;
import javax.media.opengl.*;
import java.awt.*;
import java.awt.geom.*;
import java.util.List;
/**
* Renders an icon image over the terrain surface.
*
* @author Patrick Murris
* @version $Id: SurfaceIcon.java 1772 2013-12-18 02:43:27Z tgaskins $
*/
public class SurfaceIcon extends AbstractSurfaceRenderable implements Movable
{
private Object imageSource;
private boolean useMipMaps = true;
private LatLon location;
private Vec4 locationOffset; // Pixels
private double scale = 1d;
private Angle heading = Angle.ZERO; // CW from north
private Color color = Color.WHITE;
private boolean maintainSize = false;
private double maxSize = Double.MAX_VALUE; // Meter
private double minSize = .1; // Meter
protected WWTexture texture;
protected int imageWidth = 32;
protected int imageHeight = 32;
public SurfaceIcon(Object imageSource)
{
this(imageSource, null);
}
public SurfaceIcon(Object imageSource, LatLon location)
{
this.setImageSource(imageSource);
if (location != null)
this.setLocation(location);
}
/**
* Get the icon reference location on the globe.
*
* @return the icon reference location on the globe.
*/
public LatLon getLocation()
{
return this.location;
}
/**
* Set the icon reference location on the globe.
*
* @param location the icon reference location on the globe.
*
* @throws IllegalArgumentException if location is null
.
*/
public void setLocation(LatLon location)
{
if (location == null)
{
String message = Logging.getMessage("nullValue.LatLonIsNull");
Logging.logger().severe(message);
throw new IllegalArgumentException(message);
}
this.location = location;
this.onPropertyChanged();
}
/**
* Get the icon displacement in pixels relative to the reference location. Can be null
.
*
* When null
the icon will be drawn with it's image center on top of it's reference location - see
* {@link #setLocation(LatLon)}. Otherwise the icon will be shifted of a distance equivalent to the number of pixels
* specified as x
and y
offset values. Positive values will move the icon to the right for
* x
and up for y
. Negative values will have the opposite effect.
*
* @return the icon displacement in pixels relative to the reference location.
*/
public Vec4 getLocationOffset()
{
return this.locationOffset;
}
/**
* Set the icon displacement in pixels relative to the reference location. Can be null
.
*
* When null
the icon will be drawn with it's image center on top of it's refence location - see {@link
* #setLocation(LatLon)}. Otherwise the icon will be shifted of a distance equivalent to the number of pixels
* specified as x
and y
offset values. Positive values will move the icon to the right for
* x
and up for y
. Negative values will have the opposite effect.
*
* @param locationOffset the icon displacement in pixels relative to the reference location.
*/
public void setLocationOffset(Vec4 locationOffset)
{
this.locationOffset = locationOffset; // can be null
this.onPropertyChanged();
}
/**
* Get the source for the icon image. Can be a file path to a local image or a {@link java.awt.image.BufferedImage}
* reference.
*
* @return the source for the icon image.
*/
public Object getImageSource()
{
return this.imageSource;
}
/**
* Set the source for the icon image. Can be a file path to a local image or a {@link java.awt.image.BufferedImage}
* reference.
*
* @param imageSource the source for the icon image.
*
* @throws IllegalArgumentException if 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;
this.onPropertyChanged();
}
/**
* Returns whether the icon will apply mip-map filtering to it's source image. If true
the icon image
* is drawn using mip-maps. If false
the icon is drawn without mip-maps, resulting in aliasing if the
* icon image is drawn smaller than it's native size in pixels.
*
* @return true
if the icon image is drawn with mip-map filtering; false
otherwise.
*/
public boolean isUseMipMaps()
{
return this.useMipMaps;
}
/**
* Sets whether the icon will apply mip-map filtering to it's source image. If true
the icon image is
* drawn using mip-maps. If false
the icon is drawn without mip-maps, resulting in aliasing if the icon
* image is drawn smaller than it's native size in pixels.
*
* @param useMipMaps true
if the icon image should be drawn with mip-map filtering; false
* otherwise.
*/
public void setUseMipMaps(boolean useMipMaps)
{
this.useMipMaps = useMipMaps;
this.texture = null;
this.onPropertyChanged();
}
/**
* Get the current scaling factor applied to the source image.
*
* @return the current scaling factor applied to the source image.
*/
public double getScale()
{
return this.scale;
}
/**
* Set the scaling factor to apply to the source image. A value of 1
will produce no change, a value
* greater then 1
will enlarge the image and a value smaller then 1
will reduce it.
*
* @param scale the scaling factor to apply to the source image.
*
* @throws IllegalArgumentException if scale is zero or negative.
*/
public void setScale(double scale)
{
if (scale <= 0)
{
String message = Logging.getMessage("generic.ArgumentOutOfRange", "scale must be greater then zero");
Logging.logger().severe(message);
throw new IllegalArgumentException(message);
}
this.scale = scale;
this.onPropertyChanged();
}
/**
* Get the current heading {@link Angle}, clockwise from North or null
.
*
* @return the current heading {@link Angle}, clockwise from North or null
.
*/
public Angle getHeading()
{
return this.heading;
}
/**
* Set the heading {@link Angle}, clockwise from North. Setting this value to null
will have the icon
* follow the view heading so as to always face the eye. The icon will rotate around it's reference location.
*
* @param heading the heading {@link Angle}, clockwise from North or null
.
*/
public void setHeading(Angle heading)
{
this.heading = heading; // can be null
this.onPropertyChanged();
}
/**
* Determines whether the icon constantly maintains it's apparent size. If true
the icon is constantly
* redrawn at the proper size depending on it's distance from the eye. If false
the icon will be drawn
* only once per level of the underlying tile pyramid. Thus it's apparent size will vary up to twice it's 'normal'
* dimension in between levels.
*
* @return true
if the icon constantly maintains it's apparent size.
*/
public boolean isMaintainSize()
{
return this.maintainSize;
}
/**
* Sets whether the icon constantly maintains it's apparent size. If true
the icon is constantly
* redrawn at the proper size depending on it's distance from the eye. If false
the icon will be drawn
* only once per level of the underlying tile pyramid. Thus it's apparent size will vary up to twice it's 'normal'
* dimension in between levels.
*
* @param state true
if the icon should constantly maintains it's apparent size.
*/
public void setMaintainSize(boolean state)
{
this.maintainSize = state;
}
/**
* Get the minimum size in meter the icon image is allowed to be reduced to once applied to the terrain surface.
* This limit applies to the source image largest dimension.
*
* The icon will try to maintain it's apparent size depending on it's distance from the eye and will extend over a
* rectangular area which largest dimension is bounded by the values provided with {@link #setMinSize(double)} and
* {@link #setMaxSize(double)}.
*
* @return the minimum size of the icon in meter.
*/
public double getMinSize()
{
return this.minSize;
}
/**
* Set the minimum size in meter the icon image is allowed to be reduced to once applied to the terrain surface.
* This limit applies to the source image largest dimension.
*
* The icon will try to maintain it's apparent size depending on it's distance from the eye and will extend over a
* rectangular area which largest dimension is bounded by the values provided with setMinSize(double)
* and {@link #setMaxSize(double)}.
*
* @param sizeInMeter the minimum size of the icon in meter.
*/
public void setMinSize(double sizeInMeter)
{
this.minSize = sizeInMeter;
this.onPropertyChanged();
}
/**
* Get the maximum size in meter the icon image is allowed to be enlarged to once applied to the terrain surface.
* This limit applies to the source image largest dimension.
*
* The icon will try to maintain it's apparent size depending on it's distance from the eye and will extend over a
* rectangular area which largest dimension is bounded by the values provided with {@link #setMinSize(double)} and
* {@link #setMaxSize(double)}.
*
* @return the maximum size of the icon in meter.
*/
public double getMaxSize()
{
return this.maxSize;
}
/**
* Get the maximum size in meter the icon image is allowed to be enlarged to once applied to the terrain surface.
* This limit applies to the source image largest dimension.
*
* The icon will try to maintain it's apparent size depending on it's distance from the eye and will extend over a
* rectangular area which largest dimension is bounded by the values provided with {@link #setMinSize(double)} and
* setMaxSize(double)
.
*
* @param sizeInMeter the maximum size of the icon in meter.
*/
public void setMaxSize(double sizeInMeter)
{
this.maxSize = sizeInMeter;
this.onPropertyChanged();
}
/**
* Get the {@link Color} the source image is combined with.
*
* @return the {@link Color} the source image is combined with.
*/
public Color getColor()
{
return this.color;
}
/**
* Set the {@link Color} the source image will be combined with - default to white.
*
* A non white color will mostly affect the white portions from the original image. This is mostly useful to alter
* the appearance of 'colorless' icons - which mainly contain black, white and shades of gray.
*
* @param color the {@link Color} the source image will be combined with.
*
* @throws IllegalArgumentException if color is null
.
*/
public void setColor(Color color)
{
if (color == null)
{
String message = Logging.getMessage("nullValue.ColorIsNull");
Logging.logger().severe(message);
throw new IllegalArgumentException(message);
}
this.color = color;
this.onPropertyChanged();
}
protected boolean isMaintainAppearance()
{
return this.getHeading() == null || this.isMaintainSize(); // always facing or constant size
}
// *** SurfaceObject interface ***
/**
* {@inheritDoc}
*
* Overridden to return a unique state key if the icon is configured to always redraw. SurfaceIcon does not use a
* cached representation if it's heading is configured to follow the view, or if it's configured to maintain a
* constant screen size.
*
* @see #getHeading()
* @see #isMaintainSize()
*/
@Override
public Object getStateKey(DrawContext dc)
{
// If the icon always redraws, return a unique object that is not equivalent to any other state key.
if (this.isMaintainAppearance())
return new Object();
return super.getStateKey(dc);
}
public List getSectors(DrawContext dc)
{
if (dc == null)
{
String message = Logging.getMessage("nullValue.DrawContextIsNull");
Logging.logger().severe(message);
throw new IllegalArgumentException(message);
}
return this.computeSectors(dc);
}
public Extent getExtent(DrawContext dc)
{
if (dc == null)
{
String message = Logging.getMessage("nullValue.DrawContextIsNull");
Logging.logger().severe(message);
throw new IllegalArgumentException(message);
}
return this.computeExtent(dc);
}
public void drawGeographic(DrawContext dc, SurfaceTileDrawContext sdc)
{
WWTexture texture = getTexture();
if (texture == null)
return;
this.beginDraw(dc);
try
{
if (texture.bind(dc))
{
// Update image width and height
this.imageWidth = texture.getWidth(dc);
this.imageHeight = texture.getHeight(dc);
// Apply texture local transform
GL2 gl = dc.getGL().getGL2(); // GL initialization checks for GL2 compatibility.
gl.glMatrixMode(GL.GL_TEXTURE);
this.getTexture().applyInternalTransform(dc);
// Apply draw color
this.applyDrawColor(dc);
//Draw
this.drawIcon(dc, sdc);
}
}
catch (Exception e)
{
// TODO: log error
}
finally
{
// Restore gl state
this.endDraw(dc);
}
}
protected void onPropertyChanged()
{
this.updateModifiedTime();
this.clearCaches();
}
@Override
protected void clearCaches()
{
super.clearCaches();
}
protected List computeSectors(DrawContext dc)
{
if (this.location == null)
return null;
Globe globe = dc.getGlobe();
// Compute real world icon extent depending on distance from eye
Rectangle2D.Double rect = computeDrawDimension(dc, this.location); // meter
// If the icon does not redraw all the time, double it's dimension
if (!this.isMaintainAppearance())
{
rect.setRect(rect.x, rect.y, rect.width * 2, rect.height * 2);
}
// Compute bounding sector and apply location offset to it
double cosLat = Math.max(this.location.getLatitude().cos(), .01); // avoids division by zero at the poles
double dLatRadians = rect.height / globe.getRadius();
double dLonRadians = rect.width / globe.getRadius() / cosLat;
double offsetLatRadians = locationOffset != null ? locationOffset.y * dLatRadians / this.imageHeight : 0;
double offsetLonRadians = locationOffset != null ? locationOffset.x * dLonRadians / this.imageWidth : 0;
Sector sector = new Sector(
this.location.getLatitude().subtractRadians(dLatRadians / 2).addRadians(offsetLatRadians),
this.location.getLatitude().addRadians(dLatRadians / 2).addRadians(offsetLatRadians),
this.location.getLongitude().subtractRadians(dLonRadians / 2).addRadians(offsetLonRadians),
this.location.getLongitude().addRadians(dLonRadians / 2).addRadians(offsetLonRadians)
);
// Rotate sector around location
sector = computeRotatedSectorBounds(sector, this.location, computeDrawHeading(dc));
return computeNormalizedSectors(sector);
}
protected Rectangle2D.Double computeDrawDimension(DrawContext dc, LatLon location)
{
// Compute icon extent at 1:1 depending on distance from eye
double pixelSize = computePixelSizeAtLocation(dc, location);
return computeDrawDimension(pixelSize);
}
protected Rectangle2D.Double computeDrawDimension(double pixelSize)
{
// Compute icon extent at 1:1 depending on target tile pixel size
double height = this.imageHeight * this.scale * pixelSize;
double width = this.imageWidth * this.scale * pixelSize;
// Clamp to size range
double size = height > width ? height : width;
double scale = size > this.maxSize ? this.maxSize / size : size < this.minSize ? this.minSize / size : 1;
return new Rectangle2D.Double(0, 0, width * scale, height * scale); // meter
}
protected Angle computeDrawHeading(DrawContext dc)
{
if (this.heading != null)
return this.heading;
return getViewHeading(dc);
}
protected void beginDraw(DrawContext dc)
{
GL2 gl = dc.getGL().getGL2(); // GL initialization checks for GL2 compatibility.
int attributeMask = GL2.GL_TRANSFORM_BIT // for modelview
| GL2.GL_CURRENT_BIT // for current color
| GL2.GL_COLOR_BUFFER_BIT // for alpha test func and ref, and blend
| GL2.GL_ENABLE_BIT; // for enable/disable changes
gl.glPushAttrib(attributeMask);
// Suppress any fully transparent image pixels
gl.glEnable(GL2.GL_ALPHA_TEST);
gl.glAlphaFunc(GL2.GL_GREATER, 0.001f);
gl.glMatrixMode(GL2.GL_TEXTURE);
gl.glPushMatrix();
gl.glMatrixMode(GL2.GL_MODELVIEW);
gl.glPushMatrix();
if (dc.isPickingMode())
{
// Set up to replace the non-transparent texture colors with the single pick color.
gl.glEnable(GL.GL_TEXTURE_2D);
gl.glTexEnvf(GL2.GL_TEXTURE_ENV, GL2.GL_TEXTURE_ENV_MODE, GL2.GL_COMBINE);
gl.glTexEnvf(GL2.GL_TEXTURE_ENV, GL2.GL_SRC0_RGB, GL2.GL_PREVIOUS);
gl.glTexEnvf(GL2.GL_TEXTURE_ENV, GL2.GL_COMBINE_RGB, GL2.GL_REPLACE);
}
else
{
gl.glEnable(GL.GL_TEXTURE_2D);
gl.glEnable(GL.GL_BLEND);
gl.glBlendFunc(GL.GL_ONE, GL.GL_ONE_MINUS_SRC_ALPHA);
}
}
protected void endDraw(DrawContext dc)
{
GL2 gl = dc.getGL().getGL2(); // GL initialization checks for GL2 compatibility.
if (dc.isPickingMode())
{
gl.glTexEnvf(GL2.GL_TEXTURE_ENV, GL2.GL_TEXTURE_ENV_MODE, OGLUtil.DEFAULT_TEX_ENV_MODE);
gl.glTexEnvf(GL2.GL_TEXTURE_ENV, GL2.GL_SRC0_RGB, OGLUtil.DEFAULT_SRC0_RGB);
gl.glTexEnvf(GL2.GL_TEXTURE_ENV, GL2.GL_COMBINE_RGB, OGLUtil.DEFAULT_COMBINE_RGB);
}
gl.glBindTexture(GL.GL_TEXTURE_2D, 0);
gl.glMatrixMode(GL2.GL_MODELVIEW);
gl.glPopMatrix();
gl.glMatrixMode(GL2.GL_TEXTURE);
gl.glPopMatrix();
gl.glPopAttrib();
}
@SuppressWarnings({"UnusedDeclaration"})
protected void applyDrawTransform(DrawContext dc, SurfaceTileDrawContext sdc, LatLon location, double drawScale)
{
// Compute icon viewport point
// Apply hemisphere offset if needed - for icons that may cross the date line
double offset = computeHemisphereOffset(sdc.getSector(), location);
Vec4 point = new Vec4(location.getLongitude().degrees + offset, location.getLatitude().degrees, 1);
point = point.transformBy4(sdc.getModelviewMatrix());
GL2 gl = dc.getGL().getGL2(); // GL initialization checks for GL2 compatibility.
// Translate to location point
gl.glTranslated(point.x(), point.y(), point.z());
// Add x scaling transform to maintain icon width and aspect ratio at any latitude
gl.glScaled(drawScale / location.getLatitude().cos(), drawScale, 1);
// Add rotation to account for icon heading
gl.glRotated(this.computeDrawHeading(dc).degrees, 0, 0, -1);
// Translate to lower left corner
gl.glTranslated(-this.imageWidth / 2, -this.imageHeight / 2, 0);
// Apply location offset if any
if (this.locationOffset != null)
gl.glTranslated(this.locationOffset.x, this.locationOffset.y, 0);
}
protected double computeDrawScale(DrawContext dc, SurfaceTileDrawContext sdc, LatLon location)
{
// Compute scaling to maintain apparent size
double drawPixelSize;
double regionPixelSize = this.computeDrawPixelSize(dc, sdc);
if (this.isMaintainAppearance())
// Compute precise size depending on eye distance
drawPixelSize = this.computeDrawDimension(dc, location).width / this.imageWidth;
else
// Compute size according to draw tile resolution
drawPixelSize = this.computeDrawDimension(regionPixelSize).width / this.imageWidth;
return drawPixelSize / regionPixelSize;
}
protected void applyDrawColor(DrawContext dc)
{
if (!dc.isPickingMode())
{
GL2 gl = dc.getGL().getGL2(); // GL initialization checks for GL2 compatibility.
applyPremultipliedAlphaColor(gl, this.color, getOpacity());
}
}
protected void drawIcon(DrawContext dc, SurfaceTileDrawContext sdc)
{
GL2 gl = dc.getGL().getGL2(); // GL initialization checks for GL2 compatibility.
gl.glMatrixMode(GL2.GL_MODELVIEW);
double drawScale = this.computeDrawScale(dc, sdc, this.location);
this.applyDrawTransform(dc, sdc, this.location, drawScale);
gl.glScaled(this.imageWidth, this.imageHeight, 1d);
dc.drawUnitQuad(new TextureCoords(0, 0, 1, 1));
}
protected WWTexture getTexture()
{
if (this.texture == null)
this.texture = new BasicWWTexture(this.imageSource, this.useMipMaps);
return this.texture;
}
// *** Movable interface
public Position getReferencePosition()
{
return new Position(this.location, 0);
}
public void move(Position delta)
{
if (delta == null)
{
String msg = Logging.getMessage("nullValue.PositionIsNull");
Logging.logger().severe(msg);
throw new IllegalArgumentException(msg);
}
Position referencePos = this.getReferencePosition();
if (referencePos == null)
return;
this.moveTo(referencePos.add(delta));
}
public void moveTo(Position position)
{
if (position == null)
{
String msg = Logging.getMessage("nullValue.PositionIsNull");
Logging.logger().severe(msg);
throw new IllegalArgumentException(msg);
}
this.setLocation(position);
}
//**************************************************************//
//******************** Sector Cache Info *********************//
//**************************************************************//
protected static class SectorInfo
{
protected List sectors;
protected Object globeStateKey;
public SectorInfo(List sectors, DrawContext dc)
{
// Surface icon sectors depend on the state of the globe used to compute it.
this.sectors = sectors;
this.globeStateKey = dc.getGlobe().getStateKey(dc);
}
public boolean isValid(DrawContext dc)
{
return this.globeStateKey.equals(dc.getGlobe().getStateKey(dc));
}
}
}