package renderers;
import gov.nasa.worldwind.Disposable;
import gov.nasa.worldwind.View;
import gov.nasa.worldwind.geom.Angle;
import gov.nasa.worldwind.geom.Position;
import gov.nasa.worldwind.geom.Vec4;
import gov.nasa.worldwind.pick.PickSupport;
import gov.nasa.worldwind.render.DrawContext;
import gov.nasa.worldwind.render.MultiLineTextRenderer;
import gov.nasa.worldwind.render.Renderable;
import gov.nasa.worldwind.util.OGLStackHandler;
import gov.nasa.worldwind.util.OGLTextRenderer;
import gov.nasa.worldwind.util.OGLUtil;
import java.awt.Color;
import java.awt.Font;
import java.awt.Rectangle;
import java.awt.geom.Rectangle2D;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.media.opengl.GL;
import javax.media.opengl.glu.GLU;
import org.apache.log4j.Level;
import org.apache.log4j.Logger;
import com.sun.opengl.util.j2d.TextRenderer;
/**
* Defines a render that allows to draw multiple {@link RenderableTextItem} at
* once as fast as possible.
*/
public class FastTextRenderer implements Renderable, Disposable {
/**
* The logger of the class.
*/
private static final Logger LOG = Logger.getLogger(FastTextRenderer.class);
/**
* Defines a view property cache.
*/
private static class ViewPropertyCache {
/**
* The cached cartesian point which is the center of the view.
*/
private Vec4 centerPt;
/**
* The cached heading.
*/
private Angle heading;
/**
* The cached elevation.
*/
private double elevation;
/**
* Tests whether the cache needs to be rebuild according to the given
* draw context adn viewport extent.
*
* @param dc
* the draw context.
* @param extent
* the viewport extent in pixels.
* @return true
if the cache needs to be rebuild, otherwise
* false.
*/
boolean needsRebuildCache(final DrawContext dc, final int extent) {
final View view = dc.getView();
boolean rebuild = false;
final Angle heading = view.getHeading();
rebuild = (heading.equals(this.heading) == false);
rebuild = (rebuild || (this.elevation != view.getEyePosition().elevation));
if ((rebuild == false) && (this.centerPt != null) && (view.getCenterPoint() != null)) {
final Vec4 p0 = view.project(this.centerPt);
final Vec4 p1 = view.project(view.getCenterPoint());
rebuild = (Math.abs(p1.x - p0.x) > extent) || (Math.abs(p1.y - p0.y) > extent);
}
return rebuild;
}
/**
* Push the properties of the given draw context to this cache.
*
* @param dc
* the draw context for which the properties should be
* cached.
*/
void pushProperties(final DrawContext dc) {
final View view = dc.getView();
this.heading = view.getHeading();
this.centerPt = view.getCenterPoint();
this.elevation = view.getEyePosition().elevation;
}
/**
* Applies a transform to the model view matrix in order to have a
* proper display from the cached view properties.
*
* @param dc
* the draw context to which the transform should be applied.
*/
public void applyTransform(final DrawContext dc) {
final GL gl = dc.getGL();
final View v = dc.getView();
if ((this.centerPt != null) && (v.getCenterPoint() != null)) {
final Vec4 p0 = v.project(this.centerPt);
final Vec4 p1 = v.project(v.getCenterPoint());
final Vec4 translation = p0.subtract3(p1);
if ((translation.x != 0) || (translation.y != 0)) {
gl.glMatrixMode(GL.GL_MODELVIEW);
gl.glTranslated(translation.x, translation.y, 0);
}
}
}
}
public static class RenderableText {
private String text = null;
private boolean isVisible = true;
private boolean isPickable = true;
private boolean isBBoxVisible = false;
private Position position;
private Color color;
public String getText() {
return this.text;
}
public void setText(final String text) {
this.text = text;
}
public boolean isVisible() {
return this.isVisible;
}
public void setVisible(final boolean visible) {
this.isVisible = visible;
}
public boolean isPickable() {
return this.isPickable;
}
public void setPickable(final boolean pickable) {
this.isPickable = pickable;
}
public boolean isBoundingBoxVisible() {
return this.isBBoxVisible;
}
public void setBoundingBoxVisible(final boolean visible) {
this.isBBoxVisible = visible;
}
public Position getPosition() {
return this.position;
}
public void setPosition(final Position p) {
this.position = p;
}
public Color getTextColor() {
return this.color;
}
public void setTextColor(final Color c) {
this.color = c;
}
}
public static class LabelCacheData {
/**
* The cached position.
*/
Position position;
/**
* The cached cartesian point.
*/
Vec4 cartesianPt;
/**
* The cached cartesian point.
*/
Rectangle bounds;
/**
* The cached screen bounds.
*/
public Rectangle2D screenBounds;
/**
* The cached screen position.
*/
public Vec4 screentPt;
}
/**
* The default margin that extends the viewport.
*/
private static final int DEFAULT_MARGIN = 100;
/**
* The list of texts that should be rendered on a call to
* {@link #render(DrawContext)} method.
*/
private final List labelList;
/**
* The OpenGL stack handler utility.
*/
private final OGLStackHandler oglStackHandler;
/**
* Utilities for picking.
*/
private final PickSupport pickSupport;
/**
* The map of RenderableTextItem cache data.
*/
private final Map cacheMap;
/**
* The font used to render the label.
*/
private final Font font;
/**
* The view properties cache.
*/
private final ViewPropertyCache viewPropsCache = new ViewPropertyCache();
/**
* The identifier of the OpenGL list which renders the texts.
*/
private int textListId = -1;
/**
* The identifier of the OpenGL list which renders the bounding box of the
* texts.
*/
private int boundingBoxesListId = -1;
/**
* The margin, in pixels, that extends the viewport to define the bounds
* which includes the labels that should be displayed.
*/
private final int margin = DEFAULT_MARGIN;
/**
* A flag that specifies whether the cache has been invalidate.
*/
private boolean cacheInvalidated = true;
/**
* Creates a new {@link LabelsRenderer}.
*/
public FastTextRenderer() {
this.cacheMap = new HashMap();
this.pickSupport = new PickSupport();
this.labelList = new ArrayList();
this.oglStackHandler = new OGLStackHandler();
this.font = new Font(null);
}
/**
* Releases internal objects.
*/
public void dispose() {
this.labelList.clear();
this.cacheMap.clear();
this.oglStackHandler.clear();
}
/**
* Adds the given label to the list of labels that should be rendered by
* this renderer when {@link #render(DrawContext)} method is called.
*
* @param label
* the label that should be added to the list of labels that
* should be rendered by this renderer.
*/
public void addText(final RenderableText label) {
if (labelList.contains(label) == false) {
labelList.add(label);
}
this.cacheInvalidated = true;
}
/**
* Removes the given label from the list of labels that should be rendered
* by this renderer.
*
* @param label
* the label that should be removed from the list of labels that
* should be rendered by this renderer.
*/
public void removeText(final RenderableText label) {
labelList.remove(label);
this.cacheMap.remove(label);
this.cacheInvalidated = true;
}
/**
* Invalidates the cache data for the given label.
*
* @param label
* the label for which the cache data should be invalidated.
*/
public void invalidate(final RenderableText label) {
if (this.labelList.contains(label) == true) {
this.cacheMap.remove(label);
this.cacheInvalidated = true;
}
}
@Override
public void render(final DrawContext dc) {
if (dc == null)
return;
final long start = System.currentTimeMillis();
if (dc.isPickingMode() == false) {
final boolean rebuildCache = this.viewPropsCache.needsRebuildCache(dc, 20);
final TextRenderer textRenderer = OGLTextRenderer.getOrCreateTextRenderer(dc.getTextRendererCache(), font);
final MultiLineTextRenderer renderer = new MultiLineTextRenderer(textRenderer);
if ((this.textListId == -1) || (this.boundingBoxesListId == -1) || (rebuildCache == true) || (this.cacheInvalidated == true)) {
rebuildCache(dc, renderer, new ArrayList(this.labelList));
}
final GL gl = dc.getGL();
if (this.textListId != -1) {
try {
beginRender(dc, renderer);
this.viewPropsCache.applyTransform(dc);
gl.glCallList(this.textListId);
}
catch (final Throwable t) {
if (LOG.isEnabledFor(Level.ERROR) == true) {
LOG.log(Level.ERROR, "An error occurred while rendering labels", t);
}
}
finally {
endRender(dc, renderer);
}
}
if (this.boundingBoxesListId != -1) {
try {
gl.glPolygonMode(GL.GL_FRONT_AND_BACK, GL.GL_LINE);
beginRenderBoundingBox(dc, this.oglStackHandler, null);
this.viewPropsCache.applyTransform(dc);
gl.glCallList(this.boundingBoxesListId);
}
catch (final Throwable t) {
if (LOG.isEnabledFor(Level.ERROR) == true) {
LOG.log(Level.ERROR, "An error occurred while rendering text bounding boxes", t);
}
}
finally {
endRenderBoundingBox(dc, this.oglStackHandler, this.pickSupport);
gl.glPolygonMode(GL.GL_FRONT_AND_BACK, GL.GL_FILL);
}
}
if (LOG.isEnabledFor(Level.TRACE) == true) {
final long time = System.currentTimeMillis() - start;
LOG.log(Level.TRACE, "Time to render : " + time + " ms");
}
}
else {
final ArrayList labels = new ArrayList();
for (final RenderableText label : this.labelList) {
if (label.isPickable() == true) {
labels.add(label);
}
}
beginRenderBoundingBox(dc, this.oglStackHandler, this.pickSupport);
this.viewPropsCache.applyTransform(dc);
try {
// pick labels using their bounding box
doRenderBoudingBoxes(dc, labels, this.pickSupport);
}
catch (final Throwable t) {
if (LOG.isEnabledFor(Level.ERROR) == true) {
LOG.log(Level.ERROR, "An error occurred while picking labels", t);
}
}
finally {
endRenderBoundingBox(dc, this.oglStackHandler, this.pickSupport);
}
}
}
/**
* Rebuilds the cache OpenGL list for the given labels.
*
* @param dc
* the draw context.
* @param renderer
* the text renderer.
* @param labels
* the text that should be rendered.
*/
private void rebuildCache(final DrawContext dc, final MultiLineTextRenderer renderer, final List labels) {
final ArrayList bounds = new ArrayList();
final GL gl = dc.getGL();
this.cacheMap.clear();
if (textListId != -1) {
gl.glDeleteLists(this.textListId, 1);
this.textListId = -1;
}
this.viewPropsCache.pushProperties(dc);
this.textListId = gl.glGenLists(1);
gl.glNewList(this.textListId, GL.GL_COMPILE);
renderer.getTextRenderer().setSmoothing(false);
renderer.getTextRenderer().begin3DRendering();
final Rectangle viewport = dc.getView().getViewport();
viewport.x -= margin;
viewport.y -= margin;
viewport.width += margin + margin;
viewport.height += margin + margin;
for (final RenderableText label : labels) {
if ((label.isVisible() == true) && (label.getText() != null) && (label.getText().isEmpty() == false)) {
doRenderLabel(label, dc, renderer, viewport);
if (label.isBoundingBoxVisible() == true) {
bounds.add(label);
}
}
}
renderer.getTextRenderer().end3DRendering();
gl.glEndList();
if (this.boundingBoxesListId != -1) {
gl.glDeleteLists(this.boundingBoxesListId, 1);
this.boundingBoxesListId = -1;
}
this.boundingBoxesListId = gl.glGenLists(1);
gl.glNewList(this.boundingBoxesListId, GL.GL_COMPILE);
doRenderBoudingBoxes(dc, bounds, null);
gl.glEndList();
this.cacheInvalidated = false;
}
/**
* Computes the cache data for the given label within the given draw context
* and using the given text renderer.
*
* @param label
* the label.
* @param dc
* the draw context.
* @param renderer
* the text renderer.
* @return a {@link LabelCacheData} which provides cache data for the given
* label.
*/
private LabelCacheData computeCacheData(final RenderableText label, final DrawContext dc, final MultiLineTextRenderer renderer) {
// computes the location of the text in the Cartesian system
final String text = label.getText();
Rectangle initBounds;
if (MultiLineTextRenderer.containsHTML(text) == true) {
initBounds = renderer.getBoundsHTML(text, dc.getTextRendererCache());
}
else {
initBounds = renderer.getBounds(text);
}
final double xPadding = -initBounds.getWidth() / 2.;
final double yPadding = -initBounds.getHeight();
final Rectangle bounds = new Rectangle(initBounds);
bounds.x = (int) Math.round(+xPadding);
bounds.y = (int) Math.round(+yPadding);
final LabelCacheData data = new LabelCacheData();
data.bounds = bounds;
data.position = label.getPosition();
data.cartesianPt = dc.getGlobe().computePointFromLocation(data.position);
data.screentPt = (data.cartesianPt != null) ? dc.getView().project(data.cartesianPt) : null;
if (data.screentPt != null) {
data.screentPt = data.screentPt.add3(xPadding, -yPadding, 0);
data.screenBounds = new Rectangle2D.Double();
data.screenBounds.setRect(data.screentPt.x, dc.getDrawableHeight() - data.screentPt.y + 3, initBounds.width, initBounds.height);
}
else {
data.screenBounds = null;
}
return data;
}
/**
* Renders the text in the given draw context using the given text render.
*
* @param dc
* the draw context in which the text should be rendered.
*/
private void doRenderLabel(final RenderableText label, final DrawContext dc, final MultiLineTextRenderer renderer, final Rectangle viewBounds) {
LabelCacheData data = this.cacheMap.get(label);
if (data == null) {
data = computeCacheData(label, dc, renderer);
this.cacheMap.put(label, data);
}
final Vec4 screenPt = data.screentPt;
if (screenPt == null)
return;
if (viewBounds.contains(screenPt.x, screenPt.y) == false)
return;
final Color color = label.getTextColor();
if (color != null) {
renderer.setTextColor(color);
}
final int x = (int) (screenPt.x);
final int y = (int) (screenPt.y);
final String t = label.getText();
renderer.draw(t, x, y);
}
/**
* Initializes the rendering of the text in the given draw context with the
* given text renderer.
*
* @param dc
* the draw context in which the text is about to be rendered.
* @param renderer
* the text renderer.
*/
private void beginRender(final DrawContext dc, final MultiLineTextRenderer renderer) {
final GL gl = dc.getGL();
final GLU glu = dc.getGLU();
final int attribBits = GL.GL_ENABLE_BIT // for enable/disable changes
| GL.GL_COLOR_BUFFER_BIT // for alpha test func and ref, and
// blend
| GL.GL_CURRENT_BIT // for current color
| GL.GL_DEPTH_BUFFER_BIT // for depth test, depth func, and
// depth mask
| GL.GL_TRANSFORM_BIT // for modelview and perspective
| GL.GL_VIEWPORT_BIT // for depth range
| GL.GL_TEXTURE_BIT;
this.oglStackHandler.pushAttrib(gl, attribBits);
this.oglStackHandler.pushProjectionIdentity(gl);
final Rectangle viewport = dc.getView().getViewport();
glu.gluOrtho2D(0, viewport.width, 0, viewport.height);
// this.oglStackHandler.pushTextureIdentity(gl);
// Set model view as current matrix mode
this.oglStackHandler.pushModelviewIdentity(gl);
// Enable the depth test but don't write to the depth buffer.
gl.glEnable(GL.GL_DEPTH_TEST);
gl.glDepthMask(false);
// Suppress polygon culling.
gl.glDisable(GL.GL_CULL_FACE);
// Suppress any fully transparent image pixels
gl.glEnable(GL.GL_ALPHA_TEST);
gl.glAlphaFunc(GL.GL_GREATER, 0.001f);
}
/**
* Restores the given draw context at its state before the rendering of the
* text with the given text renderer.
*
* @param dc
* the draw context for which the initial state should be
* restored.
* @param renderer
* the text renderer.
*/
private void endRender(final DrawContext dc, final MultiLineTextRenderer renderer) {
final GL gl = dc.getGL();
this.oglStackHandler.pop(gl);
}
public static void beginRenderBoundingBox(final DrawContext dc, final OGLStackHandler oglStackHandler, final PickSupport pickSupport) {
final GL gl = dc.getGL();
int attributeMask = GL.GL_DEPTH_BUFFER_BIT | GL.GL_TRANSFORM_BIT | GL.GL_VIEWPORT_BIT | GL.GL_CURRENT_BIT | GL.GL_TEXTURE_BIT | GL.GL_DEPTH_BUFFER_BIT | GL.GL_ENABLE_BIT;
// do not clear color buffer in picking mode
if (dc.isPickingMode() == false) {
attributeMask = attributeMask | GL.GL_COLOR_BUFFER_BIT;
gl.glEnable(GL.GL_BLEND);
gl.glBlendFunc(GL.GL_SRC_ALPHA, GL.GL_ONE_MINUS_SRC_ALPHA);
gl.glEnable(GL.GL_LINE_SMOOTH);
}
else if (pickSupport != null) {
gl.glDisable(GL.GL_DEPTH_TEST);
pickSupport.clearPickList();
pickSupport.beginPicking(dc);
}
oglStackHandler.pushAttrib(gl, attributeMask);
final java.awt.Rectangle viewport = dc.getView().getViewport();
oglStackHandler.pushProjectionIdentity(gl);
gl.glOrtho(0d, viewport.width, 0d, viewport.height, -1, 1);
oglStackHandler.pushModelviewIdentity(gl);
gl.glTranslated(0, viewport.height, 0);
}
public static void endRenderBoundingBox(final DrawContext dc, final OGLStackHandler oglStackHandler, final PickSupport pickSupport) {
final GL gl = dc.getGL();
if (dc.isPickingMode() == false) {
gl.glDisable(GL.GL_LINE_SMOOTH);
gl.glDisable(GL.GL_BLEND);
}
else if (pickSupport != null) {
gl.glEnable(GL.GL_DEPTH_TEST);
try {
pickSupport.resolvePick(dc, dc.getPickPoint(), null);
}
catch (final Throwable t) {
if (LOG.isEnabledFor(Level.ERROR) == true) {
LOG.log(Level.ERROR, "An error occurred while picking text", t);
}
}
pickSupport.endPicking(dc);
}
oglStackHandler.pop(gl);
}
/**
* Draws the bounding box of the given label within the given draw context.
*
* @param label
* the label for which the bounding box has to be drawn.
* @param dc
* the draw context in which the bound box has to be drawn.
* @param pickSupport
* the pick support used for render in picking mode.
*/
private void drawBoundingBox(final RenderableText label, final DrawContext dc, final PickSupport pickSupport) {
final LabelCacheData data = this.cacheMap.get(label);
if (data != null) {
final Rectangle2D bounds = data.screenBounds;
if ((bounds == null) || (bounds.isEmpty() == true))
return;
final GL gl = dc.getGL();
Color color;
if (dc.isPickingMode() == false) {
color = label.getTextColor();
}
else {
color = dc.getUniquePickColor();
}
OGLUtil.applyColor(gl, color, false);
gl.glVertex2d(bounds.getMaxX(), -bounds.getMaxY());
gl.glVertex2d(bounds.getMaxX(), -bounds.getMinY());
gl.glVertex2d(bounds.getMinX(), -bounds.getMinY());
gl.glVertex2d(bounds.getMinX(), -bounds.getMaxY());
if ((dc.isPickingMode() == true) && (pickSupport != null)) {
pickSupport.addPickableObject(color.getRGB(), label);
}
}
}
/**
* Renders the bounding boxes of the given texts within the given draw
* context.
*
* @param dc
* the draw context.
* @param labels
* the texts for which the bounding box will be rendered.
* @param pickSupport
* the pick support for picking mode.
*/
private void doRenderBoudingBoxes(final DrawContext dc, final Iterable labels, final PickSupport pickSupport) {
if ((dc.isPickingMode() == true) && (pickSupport == null))
return;
final GL gl = dc.getGL();
gl.glBegin(GL.GL_QUADS);
for (final RenderableText text : labels) {
if ((text.isVisible() == true) && (text.getText() != null) && (text.getText().isEmpty() == false)) {
drawBoundingBox(text, dc, pickSupport);
}
}
gl.glEnd();
}
}