gov.nasa.worldwind.render.SurfaceText Maven / Gradle / Ivy
The newest version!
/*
* Copyright (C) 2012 United States Government as represented by the Administrator of the
* National Aeronautics and Space Administration.
* All Rights Reserved.
*/
package gov.nasa.worldwind.render;
import gov.nasa.worldwind.*;
import gov.nasa.worldwind.avlist.AVKey;
import gov.nasa.worldwind.drag.*;
import gov.nasa.worldwind.geom.*;
import gov.nasa.worldwind.globes.Globe;
import gov.nasa.worldwind.util.*;
import com.jogamp.opengl.GL2;
import java.awt.*;
import java.awt.geom.*;
import java.util.Arrays;
/**
* Renders a string of text on the surface of the globe. The text will appear draped over terrain. Surface text is drawn
* at a constant geographic size: it will appear larger when the view zooms in on the text and smaller when the view
* zooms out.
*
* @author pabercrombie
* @version $Id: SurfaceText.java 3092 2015-05-14 22:21:32Z tgaskins $
*/
// TODO: add support for heading
public class SurfaceText extends AbstractSurfaceObject implements GeographicText, Movable, Draggable
{
/** Default text size. */
public final static double DEFAULT_TEXT_SIZE_IN_METERS = 1000;
/** Default font. */
public static final Font DEFAULT_FONT = Font.decode("Arial-BOLD-24");
/** Default text color. */
public static final Color DEFAULT_COLOR = Color.WHITE;
/** Default offset. The default offset centers the text on its geographic position both horizontally and vertically. */
public static final Offset DEFAULT_OFFSET = new Offset(-0.5d, -0.5d, AVKey.FRACTION, AVKey.FRACTION);
/** The text to draw. */
protected CharSequence text;
/** Location at which to draw the text. */
protected Position location;
/** The height of the text in meters. */
protected double textSizeInMeters = DEFAULT_TEXT_SIZE_IN_METERS;
/** Dragging Support */
protected boolean dragEnabled = true;
protected DraggableSupport draggableSupport = null;
/** Font to use to draw the text. Defaults to {@link #DEFAULT_FONT}. */
protected Font font = DEFAULT_FONT;
/** Color to use to draw the text. Defaults to {@link #DEFAULT_COLOR}. */
protected Color color = DEFAULT_COLOR;
/** Background color for the text. By default color will be generated to contrast with the text color. */
protected Color bgColor;
/** Text priority. Can be used to implement text culling. */
protected double priority;
/** Offset that specifies where to place the text in relation to it's geographic position. */
protected Offset offset = DEFAULT_OFFSET;
// Computed each time text is rendered
/** Bounds of the text in pixels. */
protected Rectangle2D textBounds;
/** Geographic size of a pixel. */
protected double pixelSizeInMeters;
/** Scaling factor applied to the text to maintain a constant geographic size. */
protected double scale;
/**
* The lower-left location of the text box after applying offset.
*/
protected LatLon drawLocation;
/**
* Indicates whether this text spans the dateline.
*/
protected boolean spansAntimeridian = false;
/**
* Create a new surface text object.
*
* @param text Text to draw.
* @param position Geographic location at which to draw the text.
*/
public SurfaceText(String text, Position position)
{
this.setText(text);
this.setPosition(position);
}
/**
* Create a new surface text object.
*
* @param text Text to draw.
* @param position Geographic location at which to draw the text.
* @param font Font to use when drawing text.
* @param color Color to use when drawing text.
*/
public SurfaceText(String text, Position position, Font font, Color color)
{
this.setText(text);
this.setPosition(position);
this.setFont(font);
this.setColor(color);
}
/** {@inheritDoc} */
public CharSequence getText()
{
return this.text;
}
/** {@inheritDoc} */
public void setText(CharSequence text)
{
if (text == null)
{
String message = Logging.getMessage("nullValue.StringIsNull");
Logging.logger().severe(message);
throw new IllegalArgumentException(message);
}
this.text = text;
this.textBounds = null; // Need to recompute bounds
this.updateModifiedTime();
}
/** {@inheritDoc} */
public Position getPosition()
{
return this.location;
}
/**
* {@inheritDoc}
*
* The offset determines how the text is placed relative to this position. The default offset centers the text on
* the position both horizontally and vertically.
*
* @see #setOffset(Offset)
*/
public void setPosition(Position position)
{
if (position == null)
{
String message = Logging.getMessage("nullValue.LatLonIsNull");
Logging.logger().severe(message);
throw new IllegalArgumentException(message);
}
this.location = position;
this.updateModifiedTime();
}
/** {@inheritDoc} */
public Font getFont()
{
return this.font;
}
/** {@inheritDoc} */
public void setFont(Font font)
{
if (font == null)
{
String message = Logging.getMessage("nullValue.FontIsNull");
Logging.logger().severe(message);
throw new IllegalArgumentException(message);
}
// Only set the font if it is different than the active font
if (!font.equals(this.font))
{
this.font = font;
this.textBounds = null; // Need to recompute bounds
this.updateModifiedTime();
}
}
/** {@inheritDoc} */
public Color getColor()
{
return this.color;
}
/** {@inheritDoc} */
public void setColor(Color color)
{
if (color == null)
{
String message = Logging.getMessage("nullValue.ColorIsNull");
Logging.logger().severe(message);
throw new IllegalArgumentException(message);
}
if (!color.equals(this.color))
{
this.color = color;
this.updateModifiedTime();
}
}
/** {@inheritDoc} */
public Color getBackgroundColor()
{
return this.bgColor;
}
/** {@inheritDoc} */
public void setBackgroundColor(Color background)
{
if (background == null)
{
String message = Logging.getMessage("nullValue.ColorIsNull");
Logging.logger().severe(message);
throw new IllegalArgumentException(message);
}
if (bgColor == null || !bgColor.equals(background))
{
this.bgColor = background;
this.updateModifiedTime();
}
}
/** {@inheritDoc} */
public void setPriority(double priority)
{
this.priority = priority;
}
/** {@inheritDoc} */
public double getPriority()
{
return this.priority;
}
/**
* Returns the text offset. The offset determines how to position the text relative to its geographic position.
*
* @return the text offset.
*
* @see #setOffset(Offset)
*/
public Offset getOffset()
{
return this.offset;
}
/**
* Specifies a location relative to the label position at which to align the label. The label text begins at the
* point indicated by the offset. An offset of (0, 0) aligns the left baseline of the text with the position. An
* offset of (-0.5, -0.5) fraction aligns the center of the text with the position.
*
* A pixel based offset is interpreted based on the geographic size of the text. For example, if the text rendered
* "normally" in two dimensions would be 20 pixels tall, and the geographic text is 100 meters tall, then each pixel
* of text corresponds to 5 meters. So an offset of 2 pixels would correspond to a geographic offset of 10 meters.
*
* @param offset Offset that controls where to position the label relative to its geographic location.
*/
public void setOffset(Offset offset)
{
if (offset == null)
{
String message = Logging.getMessage("nullValue.OffsetIsNull");
Logging.logger().severe(message);
throw new IllegalArgumentException(message);
}
if (!offset.equals(this.offset))
{
this.offset = offset;
this.updateModifiedTime();
}
}
public double getTextSize()
{
return this.textSizeInMeters;
}
public void setTextSize(double meters)
{
this.textSizeInMeters = meters;
}
/** {@inheritDoc} */
@Override
public void preRender(DrawContext dc)
{
if (this.textBounds == null)
{
this.updateTextBounds(dc);
}
super.preRender(dc);
}
/** {@inheritDoc} */
public Position getReferencePosition()
{
return new Position(this.location, 0);
}
/** {@inheritDoc} */
public void move(Position position)
{
Position refPos = this.getReferencePosition();
if (refPos == null)
return;
this.moveTo(refPos.add(position));
}
/** {@inheritDoc} */
public void moveTo(Position position)
{
this.setPosition(position);
}
@Override
public boolean isDragEnabled()
{
return this.dragEnabled;
}
@Override
public void setDragEnabled(boolean enabled)
{
this.dragEnabled = enabled;
}
@Override
public void drag(DragContext dragContext)
{
if (!this.dragEnabled)
return;
if (this.draggableSupport == null)
this.draggableSupport = new DraggableSupport(this, WorldWind.CLAMP_TO_GROUND);
this.doDrag(dragContext);
}
protected void doDrag(DragContext dragContext)
{
this.draggableSupport.dragGlobeSizeConstant(dragContext);
}
/** {@inheritDoc} */
public java.util.List getSectors(DrawContext dc)
{
if (dc == null)
{
String message = Logging.getMessage("nullValue.DrawContextIsNull");
Logging.logger().severe(message);
throw new IllegalArgumentException(message);
}
return Arrays.asList(this.computeSector(dc));
}
/** {@inheritDoc} */
protected void drawGeographic(DrawContext dc, SurfaceTileDrawContext sdc)
{
GL2 gl = dc.getGL().getGL2(); // GL initialization checks for GL2 compatibility.
OGLStackHandler ogsh = new OGLStackHandler();
ogsh.pushAttrib(gl,
GL2.GL_CURRENT_BIT // For current color (used by JOGL TextRenderer).
| GL2.GL_TRANSFORM_BIT); // For matrix mode.
ogsh.pushModelview(gl);
try
{
this.computeGeometry(dc, sdc);
if (this.isSmall())
return;
this.applyDrawTransform(dc, sdc);
this.drawText(dc);
}
finally
{
ogsh.pop(gl);
}
}
/**
* Draw the text.
*
* @param dc Current draw context.
*/
protected void drawText(DrawContext dc)
{
TextRenderer tr = this.getTextRenderer(dc);
Point2D point = this.getOffset().computeOffset(this.textBounds.getWidth(), this.textBounds.getHeight(), null,
null);
int x = (int) point.getX();
int y = (int) point.getY();
try
{
tr.begin3DRendering();
Color bgColor = this.determineBackgroundColor(this.color);
CharSequence text = this.getText();
tr.setColor(bgColor);
tr.draw(text, x + 1, y - 1);
tr.setColor(this.getColor());
tr.draw(text, x, y);
}
finally
{
tr.end3DRendering();
}
}
/**
* Compute the text size and position.
*
* @param dc Current draw context.
* @param sdc Current surface tile draw context.
*/
protected void computeGeometry(DrawContext dc, SurfaceTileDrawContext sdc)
{
// Determine the geographic size of a pixel in the tile
this.pixelSizeInMeters = this.computePixelSize(dc, sdc);
// Determine how big the text would be without scaling
double fullHeightInMeters = this.pixelSizeInMeters * this.textBounds.getHeight();
// Calculate a scale to make the text the size we want (a constant geographic size)
this.scale = this.textSizeInMeters / fullHeightInMeters;
}
/**
* Apply a transform to the GL state to draw the text at the proper location and scale.
*
* @param dc Current draw context.
* @param sdc Current surface tile draw context.
*/
protected void applyDrawTransform(DrawContext dc, SurfaceTileDrawContext sdc)
{
Vec4 point = new Vec4(this.location.getLongitude().degrees, this.location.getLatitude().degrees, 1);
// If the text box spans the anti-meridian and we're drawing tiles to the right of the anti-meridian, then we
// need to map the translation into coordinates relative to that side of the anti-meridian.
if (this.spansAntimeridian &&
Math.signum(sdc.getSector().getMinLongitude().degrees) != Math.signum(this.drawLocation.longitude.degrees)) {
point = new Vec4(this.location.getLongitude().degrees - 360, this.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());
// Apply the scaling factor to draw the text at the correct geographic size
gl.glScaled(this.scale, this.scale, 1d);
}
/**
* Determine if the text is too small to draw.
*
* @return {@code true} if the height of the text is less than one pixel.
*/
protected boolean isSmall()
{
return this.scale * this.textSizeInMeters < this.pixelSizeInMeters;
}
/**
* Compute the size of a pixel in the surface tile.
*
* @param dc Current draw context.
* @param sdc Current surface tile draw context.
*
* @return The size of a tile pixel in meters.
*/
protected double computePixelSize(DrawContext dc, SurfaceTileDrawContext sdc)
{
return dc.getGlobe().getRadius() * sdc.getSector().getDeltaLatRadians() / sdc.getViewport().height;
}
/**
* Determine the text background color. This method returns the user specified background color, or a computed
* default color if the user has not set a background color.
*
* @param color text color.
*
* @return the user specified background color, or a default color that contrasts with the text color.
*/
protected Color determineBackgroundColor(Color color)
{
// If the app specified a background color, use that.
Color bgColor = this.getBackgroundColor();
if (bgColor != null)
return bgColor;
// Otherwise compute a color that contrasts with the text color.
return this.computeBackgroundColor(color);
}
/**
* Compute a background color that contrasts with the text color.
*
* @param color text color.
*
* @return a color that contrasts with the text color.
*/
protected Color computeBackgroundColor(Color color)
{
// Otherwise compute a color that contrasts with the text color.
float[] colorArray = new float[4];
Color.RGBtoHSB(color.getRed(), color.getGreen(), color.getBlue(), colorArray);
if (colorArray[2] > 0.5)
return new Color(0, 0, 0, 0.7f);
else
return new Color(1, 1, 1, 0.7f);
}
/**
* Compute the sector covered by this surface text.
*
* @param dc Current draw context.
*
* @return The sector covered by the surface text.
*/
protected Sector[] computeSector(DrawContext dc)
{
// Compute text extent depending on distance from eye
Globe globe = dc.getGlobe();
double widthInPixels = this.textBounds.getWidth();
double heightInPixels = this.textBounds.getHeight();
double heightInMeters = this.textSizeInMeters;
double widthInMeters = heightInMeters * (widthInPixels / heightInPixels);
double radius = globe.getRadius();
double heightInRadians = heightInMeters / radius;
double widthInRadians = widthInMeters / radius;
// Compute the offset from the reference position. Convert pixels to meters based on the geographic size
// of the text.
Point2D point = this.getOffset().computeOffset(widthInPixels, heightInPixels, null, null);
double metersPerPixel = heightInMeters / heightInPixels;
double dxRadians = (point.getX() * metersPerPixel) / radius;
double dyRadians = (point.getY() * metersPerPixel) / radius;
double minLat = this.location.latitude.addRadians(dyRadians).degrees;
double maxLat = this.location.latitude.addRadians(dyRadians + heightInRadians).degrees;
double minLon = this.location.longitude.addRadians(dxRadians).degrees;
double maxLon = this.location.longitude.addRadians(dxRadians + widthInRadians).degrees;
this.drawLocation = LatLon.fromDegrees(minLat, minLon);
if (maxLon > 180) {
// Split the bounding box into two sectors, one to each side of the anti-meridian.
Sector[] sectors = new Sector[2];
sectors[0] = Sector.fromDegrees(minLat, maxLat, minLon, 180);
sectors[1] = Sector.fromDegrees(minLat, maxLat, -180, maxLon - 360);
this.spansAntimeridian = true;
return sectors;
} else {
this.spansAntimeridian = false;
return new Sector[] {Sector.fromDegrees(minLat, maxLat, minLon, maxLon)};
}
}
/**
* Get the text renderer to use to draw text.
*
* @param dc Current draw context.
*
* @return The text renderer that will be used to draw the surface text.
*/
protected TextRenderer getTextRenderer(DrawContext dc)
{
return OGLTextRenderer.getOrCreateTextRenderer(dc.getTextRendererCache(), this.getFont(), true, false, false);
}
/**
* Determine the text bounds.
*
* @param dc Current draw context.
*/
protected void updateTextBounds(DrawContext dc)
{
this.textBounds = this.getTextRenderer(dc).getBounds(this.text);
}
}
© 2015 - 2024 Weber Informatics LLC | Privacy Policy