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(); } }