diff --git a/src/gov/nasa/worldwind/formats/vpf/VPFLayer.java b/src/gov/nasa/worldwind/formats/vpf/VPFLayer.java index 888c6bc14b..df93b3fe11 100644 --- a/src/gov/nasa/worldwind/formats/vpf/VPFLayer.java +++ b/src/gov/nasa/worldwind/formats/vpf/VPFLayer.java @@ -40,7 +40,7 @@ public class VPFLayer extends AbstractLayer protected ArrayList renderableObjects = new ArrayList(); // Renderers - protected GeographicTextRenderer textRenderer = new GeographicTextRenderer(); + protected BasicGeographicTextRenderer textRenderer = new BasicGeographicTextRenderer(); protected VPFSymbolSupport symbolSupport = new VPFSymbolSupport(GeoSymConstants.GEOSYM, "image/png"); // Threaded requests diff --git a/src/gov/nasa/worldwind/layers/GraticuleSupport.java b/src/gov/nasa/worldwind/layers/GraticuleSupport.java index ba980af0b6..b732d7dfb2 100644 --- a/src/gov/nasa/worldwind/layers/GraticuleSupport.java +++ b/src/gov/nasa/worldwind/layers/GraticuleSupport.java @@ -60,7 +60,7 @@ public int hashCode() private Map namedParams = new HashMap(); private Map namedShapeAttributes = new HashMap(); private AVList defaultParams; - private GeographicTextRenderer textRenderer = new GeographicTextRenderer(); + private BasicGeographicTextRenderer textRenderer = new BasicGeographicTextRenderer(); public GraticuleSupport() { diff --git a/src/gov/nasa/worldwind/layers/TextLayer.java b/src/gov/nasa/worldwind/layers/TextLayer.java new file mode 100644 index 0000000000..917a02bf07 --- /dev/null +++ b/src/gov/nasa/worldwind/layers/TextLayer.java @@ -0,0 +1,120 @@ +/** + * Copyright (C) 2014, United States Government as represented by the + * Administrator of the National Aeronautics and Space Administration, + * All Rights Reserved. + */ +package gov.nasa.worldwind.layers; + +import gov.nasa.worldwind.render.DeclutteringTextRenderer; +import gov.nasa.worldwind.render.DrawContext; +import gov.nasa.worldwind.render.GeographicText; +import gov.nasa.worldwind.render.GeographicTextRenderer; +import gov.nasa.worldwind.util.Logging; + +import java.util.Collection; +import java.util.concurrent.ConcurrentLinkedQueue; + +/** + * Layer to support objects of type {@link GeographicText} + * + * @author Bruno Spyckerelle + * @version $Id$ + */ +public class TextLayer extends AbstractLayer +{ + protected GeographicTextRenderer textRenderer; + protected Collection geographicTexts; + + public TextLayer() + { + this.textRenderer = new DeclutteringTextRenderer(); + this.geographicTexts = new ConcurrentLinkedQueue(); + } + + public GeographicTextRenderer getGeographicTextRenderer() + { + return this.textRenderer; + } + + public void setGeographicTextRenderer(GeographicTextRenderer textRenderer) + { + this.textRenderer = textRenderer; + } + + /** + * Adds the specified text to this layer's internal collection. + * @param text {@link GeographicText} to add. + * @throws IllegalArgumentException If text is null. + */ + public void addGeographicText(GeographicText text) + { + if (text == null) + { + String msg = "nullValue.GeographicTextIsNull"; + Logging.logger().severe(msg); + throw new IllegalArgumentException(msg); + } + this.geographicTexts.add(text); + } + + public void addGeographicTexts(Iterable texts) + { + if (texts == null) + { + String msg = Logging.getMessage("nullValue.IterableIsNull"); + Logging.logger().severe(msg); + throw new IllegalArgumentException(msg); + } + for (GeographicText text : texts) + { + if (text != null) + { + this.geographicTexts.add(text); + } + } + } + + public void removeGeographicText(GeographicText text) + { + if (text == null) + { + String msg = "nullValue.GeographicTextIsNull"; + Logging.logger().severe(msg); + throw new IllegalArgumentException(msg); + } + this.geographicTexts.remove(text); + } + + public void removeGeographicTexts(Iterable texts) + { + if (texts == null) + { + String msg = Logging.getMessage("nullValue.IterableIsNull"); + Logging.logger().severe(msg); + throw new IllegalArgumentException(msg); + } + for (GeographicText text : texts) + { + if (text != null) + { + this.geographicTexts.remove(text); + } + } + } + + public void removeAllGeographicTexts() + { + this.geographicTexts.clear(); + } + + public Iterable getActiveGeographicTexts() + { + return this.geographicTexts; + } + + @Override + protected void doRender(DrawContext dc) + { + this.textRenderer.render(dc, getActiveGeographicTexts()); + } +} \ No newline at end of file diff --git a/src/gov/nasa/worldwind/render/BasicGeographicTextRenderer.java b/src/gov/nasa/worldwind/render/BasicGeographicTextRenderer.java new file mode 100644 index 0000000000..5d288dc386 --- /dev/null +++ b/src/gov/nasa/worldwind/render/BasicGeographicTextRenderer.java @@ -0,0 +1,799 @@ +/* + * 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.View; +import gov.nasa.worldwind.avlist.AVKey; +import gov.nasa.worldwind.exception.WWRuntimeException; +import gov.nasa.worldwind.geom.*; +import gov.nasa.worldwind.globes.Globe2D; +import gov.nasa.worldwind.terrain.SectorGeometryList; +import gov.nasa.worldwind.util.*; + +import javax.media.opengl.*; +import javax.media.opengl.glu.GLU; +import javax.media.opengl.glu.gl2.GLUgl2; +import java.awt.*; +import java.awt.geom.*; +import java.io.IOException; +import java.util.*; + +/** + * @author dcollins + * @version $Id: GeographicTextRenderer.java 2392 2014-10-20 20:02:44Z tgaskins $ + */ +public class BasicGeographicTextRenderer implements GeographicTextRenderer +{ + private TextRenderer lastTextRenderer = null; + private final GLU glu = new GLUgl2(); + + private static final Font DEFAULT_FONT = Font.decode("Arial-PLAIN-12"); + private static final Color DEFAULT_COLOR = Color.white; + private boolean cullText = false; + private int cullTextMargin = 0; + private String effect = AVKey.TEXT_EFFECT_SHADOW; + + // Distance scaling and fading + private double distanceMinScale = 1d; + private double distanceMaxScale = 1d; + private double distanceMinOpacity = 1d; + private boolean isDistanceScaling = false; + private double lookAtDistance = 0; + + private boolean hasJOGLv111Bug = false; + + public BasicGeographicTextRenderer() + { + } + + /** + * Determines whether overlapping text are culled. If true text items are sorted front to back + * according to their respective priority rather then back to front, and will be drawn only if they do not overlap + * an already drawn text. If false all text will be drawn back to front whether they overlap or not. + * + * @return true if overlapping text are culled. + */ + public boolean isCullTextEnabled() + { + return cullText; + } + + /** + * Set whether overlapping text should be culled. If true text items will be sorted front to back + * according to their respective priority rather then back to front, and will be drawn only if they do not overlap + * an already drawn text. If false all text will be drawn back to front whether they overlap or not. + * + * @param cullText true if overlapping text should be culled. + */ + public void setCullTextEnabled(boolean cullText) + { + this.cullText = cullText; + } + + /** + * Get the empty margin that surrounds a text item when considering it's bounding rectangle during text culling - + * see {@link #setCullTextEnabled(boolean)}. The smaller the margin, the closer text items can get before being + * considered as overlapping. + * + * @return the empty margin that surrounds a text item - in pixels. + */ + public int getCullTextMargin() + { + return this.cullTextMargin; + } + + /** + * Set the empty margin that surrounds a text item when considering it's bounding rectangle during text culling - + * see {@link #setCullTextEnabled(boolean)}. The smaller the margin, the closer text items can get before being + * considered as overlapping. + * + * @param margin the empty margin that surrounds a text item - in pixels. + */ + public void setCullTextMargin(int margin) + { + this.cullTextMargin = margin; + } + + /** + * Get the effect used to decorate the text. Can be one of {@link AVKey#TEXT_EFFECT_SHADOW} (default), {@link + * AVKey#TEXT_EFFECT_OUTLINE} or {@link AVKey#TEXT_EFFECT_NONE}. + * + * @return the effect used for text rendering. + */ + public String getEffect() + { + return this.effect; + } + + /** + * Set the effect used to decorate the text. Can be one of {@link AVKey#TEXT_EFFECT_SHADOW} (default), {@link + * AVKey#TEXT_EFFECT_OUTLINE} or {@link AVKey#TEXT_EFFECT_NONE}. + * + * @param effect the effect to use for text rendering. + */ + public void setEffect(String effect) + { + if (effect == null) + { + String msg = Logging.getMessage("nullValue.StringIsNull"); + Logging.logger().fine(msg); + throw new IllegalArgumentException(msg); + } + + this.effect = effect; + } + + /** + * Get the minimum scale that can be applied to a text item when it gets farther away from the eye than the view + * lookat point - screen center. + * + * @return the minimum scale that can be applied to a text item when it gets away from the eye. + */ + public double getDistanceMinScale() + { + return this.distanceMinScale; + } + + /** + * Set the minimum scale that can be applied to a text item when it gets farther away from the eye than the view + * lookat point - screen center. Use a value less then one to have the text 'fade' away. A value of one will have no + * effect. + * + * @param scale the minimum scale that can be applied to a text item when it gets away from the eye. + */ + public void setDistanceMinScale(double scale) + { + this.distanceMinScale = scale; + } + + /** + * Get the maximum scale that can be applied to a text item when it gets closer to the eye than the view lookat + * point - screen center. + * + * @return the maximum scale that can be applied to a text item when it closer to the eye. + */ + public double getDistanceMaxScale() + { + return this.distanceMaxScale; + } + + /** + * Set the maximum scale that can be applied to a text item when it gets closer to the eye than the view lookat + * point - screen center. Use a value greater then one to have the text magnified in the foreground. A value of one + * will have no effect. + * + * @param scale the maximum scale that can be applied to a text item when it closer to the eye. + */ + public void setDistanceMaxScale(double scale) + { + this.distanceMaxScale = scale; + } + + /** + * Get the minimum opacity that can be applied to a text item when it gets farther away from the eye than the view + * lookat point - screen center. + * + * @return the minimum opacity that can be applied to a text item when it gets away from the eye. + */ + public double getDistanceMinOpacity() + { + return this.distanceMinOpacity; + } + + /** + * Set the minimum opacity that can be applied to a text item when it gets farther away from the eye than the view + * lookat point - screen center. Use a value less then one to have the text 'fade' away. A value of one will have no + * effect. + * + * @param opacity the minimum opacity that can be applied to a text item when it gets away from the eye. + */ + public void setDistanceMinOpacity(double opacity) + { + this.distanceMinOpacity = opacity; + } + + public void render(DrawContext dc, Iterable text) + { + this.drawMany(dc, text); + } + + public void render(DrawContext dc, GeographicText text, Vec4 textPoint) + { + if (!isTextValid(text, false)) + return; + + this.drawOne(dc, text, textPoint); + } + + private void drawMany(DrawContext dc, Iterable textIterable) + { + if (dc == null) + { + String msg = Logging.getMessage("nullValue.DrawContextIsNull"); + Logging.logger().fine(msg); + throw new IllegalArgumentException(msg); + } + if (textIterable == null) + { + String msg = Logging.getMessage("nullValue.IterableIsNull"); + Logging.logger().fine(msg); + throw new IllegalArgumentException(msg); + } + + if (dc.getVisibleSector() == null) + return; + + SectorGeometryList geos = dc.getSurfaceGeometry(); + if (geos == null) + return; + + Iterator iterator = textIterable.iterator(); + if (!iterator.hasNext()) + return; + + Frustum frustumInModelCoords = dc.getView().getFrustumInModelCoordinates(); + double horizon = dc.getView().getHorizonDistance(); + + while (iterator.hasNext()) + { + GeographicText text = iterator.next(); + if (!isTextValid(text, true)) + continue; + + if (!text.isVisible()) + continue; + + if (dc.is2DGlobe()) + { + Sector limits = ((Globe2D)dc.getGlobe()).getProjection().getProjectionLimits(); + if (limits != null && !limits.contains(text.getPosition())) + continue; + } + + Angle lat = text.getPosition().getLatitude(); + Angle lon = text.getPosition().getLongitude(); + + if (!dc.getVisibleSector().contains(lat, lon)) + continue; + + Vec4 textPoint = geos.getSurfacePoint(lat, lon, + text.getPosition().getElevation() * dc.getVerticalExaggeration()); + if (textPoint == null) + continue; + + double eyeDistance = dc.getView().getEyePoint().distanceTo3(textPoint); + if (!dc.is2DGlobe() && eyeDistance > horizon) + continue; + + if (!frustumInModelCoords.contains(textPoint)) + continue; + + dc.addOrderedRenderable(new OrderedText(text, textPoint, eyeDistance)); + } + } + + private void drawOne(DrawContext dc, GeographicText text, Vec4 textPoint) + { + if (dc == null) + { + String msg = Logging.getMessage("nullValue.DrawContextIsNull"); + Logging.logger().fine(msg); + throw new IllegalArgumentException(msg); + } + if (dc.getView() == null) + { + String msg = Logging.getMessage("nullValue.ViewIsNull"); + Logging.logger().fine(msg); + throw new IllegalArgumentException(msg); + } + + if (dc.getVisibleSector() == null) + return; + + SectorGeometryList geos = dc.getSurfaceGeometry(); + if (geos == null) + return; + + if (!text.isVisible()) + return; + + if (textPoint == null) + { + if (text.getPosition() == null) + return; + + Angle lat = text.getPosition().getLatitude(); + Angle lon = text.getPosition().getLongitude(); + + if (!dc.getVisibleSector().contains(lat, lon)) + return; + + textPoint = geos.getSurfacePoint(lat, lon, + text.getPosition().getElevation() * dc.getVerticalExaggeration()); + if (textPoint == null) + return; + } + + double horizon = dc.getView().getHorizonDistance(); + double eyeDistance = dc.getView().getEyePoint().distanceTo3(textPoint); + if (!dc.is2DGlobe() && eyeDistance > horizon) + return; + + if (!dc.getView().getFrustumInModelCoordinates().contains(textPoint)) + return; + + dc.addOrderedRenderable(new OrderedText(text, textPoint, eyeDistance)); + } + + protected static boolean isTextValid(GeographicText text, boolean checkPosition) + { + if (text == null || text.getText() == null) + return false; + + //noinspection RedundantIfStatement + if (checkPosition && text.getPosition() == null) + return false; + + return true; + } + + protected class OrderedText implements OrderedRenderable, Comparable + { + GeographicText text; + Vec4 point; + double eyeDistance; + + OrderedText(GeographicText text, Vec4 point, double eyeDistance) + { + this.text = text; + this.point = point; + this.eyeDistance = eyeDistance; + } + + // When overlapping text are culled we want to sort them front to back by priority. + public int compareTo(OrderedText t) + { + if (t.text.getPriority() - this.text.getPriority() == 0) + { + return (int) (this.eyeDistance - t.eyeDistance); + } + else + return (int) (t.text.getPriority() - this.text.getPriority()); + } + + public double getDistanceFromEye() + { + return this.eyeDistance; + } + + private BasicGeographicTextRenderer getRenderer() + { + return BasicGeographicTextRenderer.this; + } + + public void render(DrawContext dc) + { + BasicGeographicTextRenderer.this.beginRendering(dc); + try + { + if (cullText) + { + ArrayList textList = new ArrayList(); + textList.add(this); + + // Draw as many as we can in a batch to save ogl state switching. + Object nextItem = dc.peekOrderedRenderables(); + while (nextItem != null && nextItem instanceof OrderedText) + { + OrderedText ot = (OrderedText) nextItem; + if (ot.getRenderer() != BasicGeographicTextRenderer.this) + break; + + textList.add(ot); + dc.pollOrderedRenderables(); // take it off the queue + nextItem = dc.peekOrderedRenderables(); + } + + Collections.sort(textList); // sort for rendering priority then front to back + + ArrayList textBounds = new ArrayList(); + for (OrderedText ot : textList) + { + double[] scaleAndOpacity = BasicGeographicTextRenderer.this.computeDistanceScaleAndOpacity(dc, ot); + Rectangle2D newBounds = BasicGeographicTextRenderer.this.computeTextBounds(dc, ot, + scaleAndOpacity[0]); + if (newBounds == null) + continue; + + boolean overlap = false; + newBounds = BasicGeographicTextRenderer.this.computeExpandedBounds(newBounds, cullTextMargin); + for (Rectangle2D rect : textBounds) + { + if (rect.intersects(newBounds)) + overlap = true; + } + + if (!overlap) + { + textBounds.add(newBounds); + BasicGeographicTextRenderer.this.drawText(dc, ot, scaleAndOpacity[0], scaleAndOpacity[1]); + } + } + } + else //just draw each label + { + double[] scaleAndOpacity = BasicGeographicTextRenderer.this.computeDistanceScaleAndOpacity(dc, this); + BasicGeographicTextRenderer.this.drawText(dc, this, scaleAndOpacity[0], scaleAndOpacity[1]); + // Draw as many as we can in a batch to save ogl state switching. + Object nextItem = dc.peekOrderedRenderables(); + while (nextItem != null && nextItem instanceof OrderedText) + { + OrderedText ot = (OrderedText) nextItem; + if (ot.getRenderer() != BasicGeographicTextRenderer.this) + break; + + scaleAndOpacity = BasicGeographicTextRenderer.this.computeDistanceScaleAndOpacity(dc, ot); + BasicGeographicTextRenderer.this.drawText(dc, ot, scaleAndOpacity[0], scaleAndOpacity[1]); + dc.pollOrderedRenderables(); // take it off the queue + nextItem = dc.peekOrderedRenderables(); + } + } + } + catch (WWRuntimeException e) + { + Logging.logger().log(java.util.logging.Level.SEVERE, "generic.ExceptionWhileRenderingText", e); + } + catch (Exception e) + { + Logging.logger().log(java.util.logging.Level.SEVERE, "generic.ExceptionWhileRenderingText", e); + } + finally + { + BasicGeographicTextRenderer.this.endRendering(dc); + } + } + + public void pick(DrawContext dc, java.awt.Point pickPoint) + { + } + } + + protected Rectangle2D computeTextBounds(DrawContext dc, OrderedText uText, double scale) throws Exception + { + GeographicText geographicText = uText.text; + + final CharSequence charSequence = geographicText.getText(); + if (charSequence == null) + return null; + + final Vec4 screenPoint = dc.getView().project(uText.point); + if (screenPoint == null) + return null; + + Font font = geographicText.getFont(); + if (font == null) + font = DEFAULT_FONT; + + try + { + TextRenderer textRenderer = OGLTextRenderer.getOrCreateTextRenderer(dc.getTextRendererCache(), font); + if (textRenderer != this.lastTextRenderer) + { + if (this.lastTextRenderer != null) + this.lastTextRenderer.end3DRendering(); + textRenderer.begin3DRendering(); + this.lastTextRenderer = textRenderer; + } + + Rectangle2D textBound = textRenderer.getBounds(charSequence); + double x = screenPoint.x - textBound.getWidth() / 2d; + Rectangle2D bounds = new Rectangle2D.Float(); + bounds.setRect(x, screenPoint.y, textBound.getWidth(), textBound.getHeight()); + + return computeScaledBounds(bounds, scale); + } + catch (Exception e) + { + handleTextRendererExceptions(e); + return null; + } + } + + protected Rectangle2D computeScaledBounds(Rectangle2D bounds, double scale) + { + if (scale == 1) + return bounds; + + // Scale rectangle from bottom center + double halfWidth = bounds.getWidth() / 2; + bounds.setRect(bounds.getX() + halfWidth - halfWidth * scale, bounds.getY(), + bounds.getWidth() * scale, bounds.getHeight() * scale); + return bounds; + } + + protected Rectangle2D computeExpandedBounds(Rectangle2D bounds, int margin) + { + if (margin == 0) + return bounds; + + // Add margin around rectangle + bounds.setRect(bounds.getX() - margin, bounds.getY() - margin, + bounds.getWidth() + margin * 2, bounds.getHeight() + margin * 2); + return bounds; + } + + @SuppressWarnings({"UnusedDeclaration"}) + protected double[] computeDistanceScaleAndOpacity(DrawContext dc, OrderedText ot) + { + if (!this.isDistanceScaling) + return new double[] {1, 1}; + + // Determine scale and opacity factors based on distance from eye vs the distance to the look at point. + double lookAtDistance = this.lookAtDistance; + double eyeDistance = ot.getDistanceFromEye(); + double distanceFactor = Math.sqrt(lookAtDistance / eyeDistance); + double scale = WWMath.clamp(distanceFactor, + this.getDistanceMinScale(), this.getDistanceMaxScale()); + double opacity = WWMath.clamp(distanceFactor, + this.getDistanceMinOpacity(), 1); + + return new double[] {scale, opacity}; + } + + protected double computeLookAtDistance(DrawContext dc) + { + View view = dc.getView(); + + // Get point in the middle of the screen + // TODO: Get a point on the surface rather then the geoid + Position groundPos = view.computePositionFromScreenPoint( + view.getViewport().getCenterX(), view.getViewport().getCenterY()); + + // Update look at distance if center point found + if (groundPos != null) + { + // Compute distance from eye to the position in the middle of the screen + this.lookAtDistance = view.getEyePoint().distanceTo3(dc.getGlobe().computePointFromPosition(groundPos)); + } + + return this.lookAtDistance; + } + + protected void beginRendering(DrawContext dc) + { + GL2 gl = dc.getGL().getGL2(); // GL initialization checks for GL2 compatibility. + int attribBits = + GL2.GL_ENABLE_BIT // for enable/disable changes + | GL2.GL_COLOR_BUFFER_BIT // for alpha test func and ref, and blend + | GL2.GL_CURRENT_BIT // for current color + | GL2.GL_DEPTH_BUFFER_BIT // for depth test, depth func, and depth mask + | GL2.GL_TRANSFORM_BIT // for modelview and perspective + | GL2.GL_VIEWPORT_BIT; // for depth range + gl.glPushAttrib(attribBits); + + gl.glMatrixMode(GL2.GL_PROJECTION); + gl.glPushMatrix(); + gl.glLoadIdentity(); + glu.gluOrtho2D(0, dc.getView().getViewport().width, 0, dc.getView().getViewport().height); + gl.glMatrixMode(GL2.GL_MODELVIEW); + gl.glPushMatrix(); + gl.glLoadIdentity(); + gl.glMatrixMode(GL2.GL_TEXTURE); + gl.glPushMatrix(); + gl.glLoadIdentity(); + // Set model view as current matrix mode + gl.glMatrixMode(GL2.GL_MODELVIEW); + + // 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(GL2.GL_ALPHA_TEST); + gl.glAlphaFunc(GL2.GL_GREATER, 0.001f); + + // Cache distance scaling values + this.isDistanceScaling = this.getDistanceMinScale() != 1 || this.getDistanceMaxScale() != 1 + || this.distanceMinOpacity != 1; + this.computeLookAtDistance(dc); + } + + protected void endRendering(DrawContext dc) + { + if (this.lastTextRenderer != null) + { + this.lastTextRenderer.end3DRendering(); + this.lastTextRenderer = null; + } + + GL2 gl = dc.getGL().getGL2(); // GL initialization checks for GL2 compatibility. + + gl.glMatrixMode(GL2.GL_PROJECTION); + gl.glPopMatrix(); + gl.glMatrixMode(GL2.GL_MODELVIEW); + gl.glPopMatrix(); + gl.glMatrixMode(GL2.GL_TEXTURE); + gl.glPopMatrix(); + + gl.glPopAttrib(); + } + + protected Vec4 drawText(DrawContext dc, OrderedText uText, double scale, double opacity) throws Exception + { + if (uText.point == null) + { + String msg = Logging.getMessage("nullValue.PointIsNull"); + Logging.logger().fine(msg); + return null; + } + + GeographicText geographicText = uText.text; + GL2 gl = dc.getGL().getGL2(); // GL initialization checks for GL2 compatibility. + + final CharSequence charSequence = geographicText.getText(); + if (charSequence == null) + return null; + + final Vec4 screenPoint = dc.getView().project(uText.point); + if (screenPoint == null) + return null; + + Font font = geographicText.getFont(); + if (font == null) + font = DEFAULT_FONT; + + try + { + TextRenderer textRenderer = OGLTextRenderer.getOrCreateTextRenderer(dc.getTextRendererCache(), font); + if (textRenderer != this.lastTextRenderer) + { + if (this.lastTextRenderer != null) + this.lastTextRenderer.end3DRendering(); + textRenderer.begin3DRendering(); + this.lastTextRenderer = textRenderer; + } + + this.setDepthFunc(dc, uText, screenPoint); + + Rectangle2D textBounds = textRenderer.getBounds( + charSequence);//note:may already be calculated during culling + textBounds = this.computeScaledBounds(textBounds, scale); + Point.Float drawPoint = computeDrawPoint(dc, textBounds, screenPoint); + + if (drawPoint != null) + { + if (scale != 1d) + { + gl.glScaled(scale, scale, 1d); + drawPoint.setLocation(drawPoint.x / (float) scale, drawPoint.y / (float) scale); + } + + Color color = geographicText.getColor(); + if (color == null) + color = DEFAULT_COLOR; + color = this.applyOpacity(color, opacity); + + Offset offset = geographicText.getOffset(); + float xOffset = offset.getX().floatValue(); + float yOffset = offset.getY().floatValue(); + + Color background = geographicText.getBackgroundColor(); + if (background != null) + { + background = this.applyOpacity(background, opacity); + textRenderer.setColor(background); + if (this.effect.equals(AVKey.TEXT_EFFECT_SHADOW)) + { + textRenderer.draw3D(charSequence, drawPoint.x + xOffset + 1, drawPoint.y + yOffset - 1, 0, 1); + } + else if (this.effect.equals(AVKey.TEXT_EFFECT_OUTLINE)) + { + textRenderer.draw3D(charSequence, drawPoint.x + xOffset + 1, drawPoint.y + yOffset - 1, 0, 1); + textRenderer.draw3D(charSequence, drawPoint.x + xOffset + 1, drawPoint.y + yOffset + 1, 0, 1); + textRenderer.draw3D(charSequence, drawPoint.x + xOffset - 1, drawPoint.y + yOffset - 1, 0, 1); + textRenderer.draw3D(charSequence, drawPoint.x + xOffset - 1, drawPoint.y + yOffset + 1, 0, 1); + } + } + + textRenderer.setColor(color); + textRenderer.draw3D(charSequence, drawPoint.x + xOffset, drawPoint.y + yOffset, 0, 1); + textRenderer.flush(); + + if (scale != 1d) + gl.glLoadIdentity(); + } + } + catch (Exception e) + { + handleTextRendererExceptions(e); + } + + return screenPoint; + } + + protected Color applyOpacity(Color color, double opacity) + { + if (opacity >= 1) + return color; + + float[] compArray = color.getRGBComponents(null); + return new Color(compArray[0], compArray[1], compArray[2], compArray[3] * (float) opacity); + } + + private void handleTextRendererExceptions(Exception e) throws Exception + { + if (e instanceof IOException) + { + if (!this.hasJOGLv111Bug) + { + // This is likely a known JOGL 1.1.1 bug - see AMZN-287 or 343 + // Log once and then ignore. + Logging.logger().log(java.util.logging.Level.SEVERE, "generic.ExceptionWhileRenderingText", e); + this.hasJOGLv111Bug = true; + } + } + else + { + throw e; + } + } + + /** + * Computes the final draw point for the given rectangle lower left corner and target screen point. If the returned + * point is null the text will not be drawn. + * + * @param dc the current {@link DrawContext} + * @param rect the text rectangle to draw. + * @param screenPoint the projected screen point the text relates to. + * + * @return the final draw point for the given rectangle lower left corner or null. + */ + @SuppressWarnings({"UnusedDeclaration"}) + protected Point.Float computeDrawPoint(DrawContext dc, Rectangle2D rect, Vec4 screenPoint) + { + return new Point.Float((float) (screenPoint.x - rect.getWidth() / 2d), (float) (screenPoint.y)); + } + + @SuppressWarnings({"UnusedDeclaration"}) + protected void setDepthFunc(DrawContext dc, OrderedText uText, Vec4 screenPoint) + { + GL gl = dc.getGL(); + + //if (uText.text.isAlwaysOnTop()) + //{ + // gl.glDepthFunc(GL.GL_ALWAYS); + // return; + //} + + Position eyePos = dc.getView().getEyePosition(); + if (eyePos == null) + { + gl.glDepthFunc(GL.GL_ALWAYS); + return; + } + + double altitude = eyePos.getElevation(); + if (altitude < (dc.getGlobe().getMaxElevation() * dc.getVerticalExaggeration())) + { + double depth = screenPoint.z - (8d * 0.00048875809d); + depth = depth < 0d ? 0d : (depth > 1d ? 1d : depth); + gl.glDepthFunc(GL.GL_LESS); + gl.glDepthRange(depth, depth); + } + //else if (screenPoint.z >= 1d) + //{ + // gl.glDepthFunc(GL.GL_EQUAL); + // gl.glDepthRange(1d, 1d); + //} + else + { + gl.glDepthFunc(GL.GL_ALWAYS); + } + } +} diff --git a/src/gov/nasa/worldwind/render/DeclutteringTextRenderer.java b/src/gov/nasa/worldwind/render/DeclutteringTextRenderer.java index 6028c63a65..e2c9fd84a5 100644 --- a/src/gov/nasa/worldwind/render/DeclutteringTextRenderer.java +++ b/src/gov/nasa/worldwind/render/DeclutteringTextRenderer.java @@ -6,6 +6,7 @@ package gov.nasa.worldwind.render; +import gov.nasa.worldwind.avlist.AVKey; import gov.nasa.worldwind.geom.*; import gov.nasa.worldwind.globes.Globe2D; import gov.nasa.worldwind.terrain.SectorGeometryList; @@ -20,18 +21,20 @@ import java.util.Iterator; /** - * A simplified version of {@link GeographicTextRenderer} that participates in globe text decluttering. See {@link - * ClutterFilter} for more information on decluttering. + * A simplified version of {@link BasicGeographicTextRenderer} that participates in globe text decluttering. + * See {@link ClutterFilter} for more information on decluttering. * * @author tag * @version $Id: DeclutteringTextRenderer.java 2392 2014-10-20 20:02:44Z tgaskins $ */ -public class DeclutteringTextRenderer +public class DeclutteringTextRenderer implements GeographicTextRenderer { protected static final Font DEFAULT_FONT = Font.decode("Arial-PLAIN-12"); protected static final Color DEFAULT_COLOR = Color.white; protected final GLU glu = new GLUgl2(); + + private String effect = AVKey.TEXT_EFFECT_SHADOW; // Flag indicating a JOGL text rendering problem. Set to avoid continual exception logging. protected boolean hasJOGLv111Bug = false; @@ -40,6 +43,35 @@ public Font getDefaultFont() { return DEFAULT_FONT; } + + /** + * Get the effect used to decorate the text. Can be one of {@link AVKey#TEXT_EFFECT_SHADOW} (default), {@link + * AVKey#TEXT_EFFECT_OUTLINE} or {@link AVKey#TEXT_EFFECT_NONE}. + * + * @return the effect used for text rendering. + */ + public String getEffect() + { + return this.effect; + } + + /** + * Set the effect used to decorate the text. Can be one of {@link AVKey#TEXT_EFFECT_SHADOW} (default), {@link + * AVKey#TEXT_EFFECT_OUTLINE} or {@link AVKey#TEXT_EFFECT_NONE}. + * + * @param effect the effect to use for text rendering. + */ + public void setEffect(String effect) + { + if (effect == null) + { + String msg = Logging.getMessage("nullValue.StringIsNull"); + Logging.logger().fine(msg); + throw new IllegalArgumentException(msg); + } + + this.effect = effect; + } /** * Returns either a cached or new text renderer for a specified font. @@ -245,17 +277,31 @@ protected Vec4 drawText(DrawContext dc, DeclutterableText uText, double scale, d if (color == null) color = DEFAULT_COLOR; color = this.applyOpacity(color, opacity); + + Offset offset = geographicText.getOffset(); + float xOffset = offset.getX().floatValue(); + float yOffset = offset.getY().floatValue(); Color background = geographicText.getBackgroundColor(); if (background != null) { background = this.applyOpacity(background, opacity); textRenderer.setColor(background); - textRenderer.draw3D(charSequence, drawPoint.x + 1, drawPoint.y - 1, 0, 1); + if (this.effect.equals(AVKey.TEXT_EFFECT_SHADOW)) + { + textRenderer.draw3D(charSequence, drawPoint.x + xOffset + 1, drawPoint.y + yOffset - 1, 0, 1); + } + else if (this.effect.equals(AVKey.TEXT_EFFECT_OUTLINE)) + { + textRenderer.draw3D(charSequence, drawPoint.x + xOffset + 1, drawPoint.y + yOffset - 1, 0, 1); + textRenderer.draw3D(charSequence, drawPoint.x + xOffset + 1, drawPoint.y + yOffset + 1, 0, 1); + textRenderer.draw3D(charSequence, drawPoint.x + xOffset - 1, drawPoint.y + yOffset - 1, 0, 1); + textRenderer.draw3D(charSequence, drawPoint.x + xOffset - 1, drawPoint.y + yOffset + 1, 0, 1); + } } textRenderer.setColor(color); - textRenderer.draw3D(charSequence, drawPoint.x, drawPoint.y, 0, 1); + textRenderer.draw3D(charSequence, drawPoint.x + xOffset, drawPoint.y + yOffset, 0, 1); textRenderer.flush(); if (scale != 1d) @@ -356,8 +402,9 @@ protected Rectangle2D computeTextBounds(DrawContext dc, DeclutterableText text) Rectangle2D textBound = textRenderer.getBounds(charSequence); double x = screenPoint.x - textBound.getWidth() / 2d; + Offset offset = geographicText.getOffset(); Rectangle2D bounds = new Rectangle2D.Float(); - bounds.setRect(x, screenPoint.y, textBound.getWidth(), textBound.getHeight()); + bounds.setRect(x + offset.x, screenPoint.y + offset.y, textBound.getWidth(), textBound.getHeight()); return bounds; } diff --git a/src/gov/nasa/worldwind/render/GeographicText.java b/src/gov/nasa/worldwind/render/GeographicText.java index da708f0d93..a4cb23e9ba 100644 --- a/src/gov/nasa/worldwind/render/GeographicText.java +++ b/src/gov/nasa/worldwind/render/GeographicText.java @@ -116,4 +116,21 @@ public interface GeographicText * @param d New priority. */ void setPriority(double d); + + /** + * Returns the text offset. The offset determines how to position the text relative to its geographic position. + * + * @return the text offset. + * + * @see #setOffset(Offset) + */ + Offset getOffset(); + + /** + * 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. + * + * @param offset Offset that controls where to position the label relative to its geographic location. + */ + void setOffset(Offset offset); } diff --git a/src/gov/nasa/worldwind/render/GeographicTextRenderer.java b/src/gov/nasa/worldwind/render/GeographicTextRenderer.java index 9d52742bc0..2d23555399 100644 --- a/src/gov/nasa/worldwind/render/GeographicTextRenderer.java +++ b/src/gov/nasa/worldwind/render/GeographicTextRenderer.java @@ -1,795 +1,15 @@ -/* - * Copyright (C) 2012 United States Government as represented by the Administrator of the - * National Aeronautics and Space Administration. +/** + * Copyright (C) 2014, 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.View; -import gov.nasa.worldwind.avlist.AVKey; -import gov.nasa.worldwind.exception.WWRuntimeException; -import gov.nasa.worldwind.geom.*; -import gov.nasa.worldwind.globes.Globe2D; -import gov.nasa.worldwind.terrain.SectorGeometryList; -import gov.nasa.worldwind.util.*; - -import javax.media.opengl.*; -import javax.media.opengl.glu.GLU; -import javax.media.opengl.glu.gl2.GLUgl2; -import java.awt.*; -import java.awt.geom.*; -import java.io.IOException; -import java.util.*; - -/** - * @author dcollins - * @version $Id: GeographicTextRenderer.java 2392 2014-10-20 20:02:44Z tgaskins $ - */ -public class GeographicTextRenderer +public interface GeographicTextRenderer { - private TextRenderer lastTextRenderer = null; - private final GLU glu = new GLUgl2(); - - private static final Font DEFAULT_FONT = Font.decode("Arial-PLAIN-12"); - private static final Color DEFAULT_COLOR = Color.white; - private boolean cullText = false; - private int cullTextMargin = 0; - private String effect = AVKey.TEXT_EFFECT_SHADOW; - - // Distance scaling and fading - private double distanceMinScale = 1d; - private double distanceMaxScale = 1d; - private double distanceMinOpacity = 1d; - private boolean isDistanceScaling = false; - private double lookAtDistance = 0; - - private boolean hasJOGLv111Bug = false; - - public GeographicTextRenderer() - { - } - - /** - * Determines whether overlapping text are culled. If true text items are sorted front to back - * according to their respective priority rather then back to front, and will be drawn only if they do not overlap - * an already drawn text. If false all text will be drawn back to front whether they overlap or not. - * - * @return true if overlapping text are culled. - */ - public boolean isCullTextEnabled() - { - return cullText; - } - - /** - * Set whether overlapping text should be culled. If true text items will be sorted front to back - * according to their respective priority rather then back to front, and will be drawn only if they do not overlap - * an already drawn text. If false all text will be drawn back to front whether they overlap or not. - * - * @param cullText true if overlapping text should be culled. - */ - public void setCullTextEnabled(boolean cullText) - { - this.cullText = cullText; - } - - /** - * Get the empty margin that surrounds a text item when considering it's bounding rectangle during text culling - - * see {@link #setCullTextEnabled(boolean)}. The smaller the margin, the closer text items can get before being - * considered as overlapping. - * - * @return the empty margin that surrounds a text item - in pixels. - */ - public int getCullTextMargin() - { - return this.cullTextMargin; - } - - /** - * Set the empty margin that surrounds a text item when considering it's bounding rectangle during text culling - - * see {@link #setCullTextEnabled(boolean)}. The smaller the margin, the closer text items can get before being - * considered as overlapping. - * - * @param margin the empty margin that surrounds a text item - in pixels. - */ - public void setCullTextMargin(int margin) - { - this.cullTextMargin = margin; - } - - /** - * Get the effect used to decorate the text. Can be one of {@link AVKey#TEXT_EFFECT_SHADOW} (default), {@link - * AVKey#TEXT_EFFECT_OUTLINE} or {@link AVKey#TEXT_EFFECT_NONE}. - * - * @return the effect used for text rendering. - */ - public String getEffect() - { - return this.effect; - } - - /** - * Set the effect used to decorate the text. Can be one of {@link AVKey#TEXT_EFFECT_SHADOW} (default), {@link - * AVKey#TEXT_EFFECT_OUTLINE} or {@link AVKey#TEXT_EFFECT_NONE}. - * - * @param effect the effect to use for text rendering. - */ - public void setEffect(String effect) - { - if (effect == null) - { - String msg = Logging.getMessage("nullValue.StringIsNull"); - Logging.logger().fine(msg); - throw new IllegalArgumentException(msg); - } - - this.effect = effect; - } - - /** - * Get the minimum scale that can be applied to a text item when it gets farther away from the eye than the view - * lookat point - screen center. - * - * @return the minimum scale that can be applied to a text item when it gets away from the eye. - */ - public double getDistanceMinScale() - { - return this.distanceMinScale; - } - - /** - * Set the minimum scale that can be applied to a text item when it gets farther away from the eye than the view - * lookat point - screen center. Use a value less then one to have the text 'fade' away. A value of one will have no - * effect. - * - * @param scale the minimum scale that can be applied to a text item when it gets away from the eye. - */ - public void setDistanceMinScale(double scale) - { - this.distanceMinScale = scale; - } - - /** - * Get the maximum scale that can be applied to a text item when it gets closer to the eye than the view lookat - * point - screen center. - * - * @return the maximum scale that can be applied to a text item when it closer to the eye. - */ - public double getDistanceMaxScale() - { - return this.distanceMaxScale; - } - - /** - * Set the maximum scale that can be applied to a text item when it gets closer to the eye than the view lookat - * point - screen center. Use a value greater then one to have the text magnified in the foreground. A value of one - * will have no effect. - * - * @param scale the maximum scale that can be applied to a text item when it closer to the eye. - */ - public void setDistanceMaxScale(double scale) - { - this.distanceMaxScale = scale; - } - - /** - * Get the minimum opacity that can be applied to a text item when it gets farther away from the eye than the view - * lookat point - screen center. - * - * @return the minimum opacity that can be applied to a text item when it gets away from the eye. - */ - public double getDistanceMinOpacity() - { - return this.distanceMinOpacity; - } - - /** - * Set the minimum opacity that can be applied to a text item when it gets farther away from the eye than the view - * lookat point - screen center. Use a value less then one to have the text 'fade' away. A value of one will have no - * effect. - * - * @param opacity the minimum opacity that can be applied to a text item when it gets away from the eye. - */ - public void setDistanceMinOpacity(double opacity) - { - this.distanceMinOpacity = opacity; - } - - public void render(DrawContext dc, Iterable text) - { - this.drawMany(dc, text); - } - - public void render(DrawContext dc, GeographicText text, Vec4 textPoint) - { - if (!isTextValid(text, false)) - return; - - this.drawOne(dc, text, textPoint); - } - - private void drawMany(DrawContext dc, Iterable textIterable) - { - if (dc == null) - { - String msg = Logging.getMessage("nullValue.DrawContextIsNull"); - Logging.logger().fine(msg); - throw new IllegalArgumentException(msg); - } - if (textIterable == null) - { - String msg = Logging.getMessage("nullValue.IterableIsNull"); - Logging.logger().fine(msg); - throw new IllegalArgumentException(msg); - } - - if (dc.getVisibleSector() == null) - return; - - SectorGeometryList geos = dc.getSurfaceGeometry(); - if (geos == null) - return; - - Iterator iterator = textIterable.iterator(); - if (!iterator.hasNext()) - return; - - Frustum frustumInModelCoords = dc.getView().getFrustumInModelCoordinates(); - double horizon = dc.getView().getHorizonDistance(); - - while (iterator.hasNext()) - { - GeographicText text = iterator.next(); - if (!isTextValid(text, true)) - continue; - - if (!text.isVisible()) - continue; - - if (dc.is2DGlobe()) - { - Sector limits = ((Globe2D)dc.getGlobe()).getProjection().getProjectionLimits(); - if (limits != null && !limits.contains(text.getPosition())) - continue; - } - - Angle lat = text.getPosition().getLatitude(); - Angle lon = text.getPosition().getLongitude(); - - if (!dc.getVisibleSector().contains(lat, lon)) - continue; - - Vec4 textPoint = geos.getSurfacePoint(lat, lon, - text.getPosition().getElevation() * dc.getVerticalExaggeration()); - if (textPoint == null) - continue; - - double eyeDistance = dc.getView().getEyePoint().distanceTo3(textPoint); - if (!dc.is2DGlobe() && eyeDistance > horizon) - continue; - - if (!frustumInModelCoords.contains(textPoint)) - continue; - - dc.addOrderedRenderable(new OrderedText(text, textPoint, eyeDistance)); - } - } - - private void drawOne(DrawContext dc, GeographicText text, Vec4 textPoint) - { - if (dc == null) - { - String msg = Logging.getMessage("nullValue.DrawContextIsNull"); - Logging.logger().fine(msg); - throw new IllegalArgumentException(msg); - } - if (dc.getView() == null) - { - String msg = Logging.getMessage("nullValue.ViewIsNull"); - Logging.logger().fine(msg); - throw new IllegalArgumentException(msg); - } - - if (dc.getVisibleSector() == null) - return; - - SectorGeometryList geos = dc.getSurfaceGeometry(); - if (geos == null) - return; - - if (!text.isVisible()) - return; - - if (textPoint == null) - { - if (text.getPosition() == null) - return; - - Angle lat = text.getPosition().getLatitude(); - Angle lon = text.getPosition().getLongitude(); - - if (!dc.getVisibleSector().contains(lat, lon)) - return; - - textPoint = geos.getSurfacePoint(lat, lon, - text.getPosition().getElevation() * dc.getVerticalExaggeration()); - if (textPoint == null) - return; - } - - double horizon = dc.getView().getHorizonDistance(); - double eyeDistance = dc.getView().getEyePoint().distanceTo3(textPoint); - if (!dc.is2DGlobe() && eyeDistance > horizon) - return; - - if (!dc.getView().getFrustumInModelCoordinates().contains(textPoint)) - return; - - dc.addOrderedRenderable(new OrderedText(text, textPoint, eyeDistance)); - } - - protected static boolean isTextValid(GeographicText text, boolean checkPosition) - { - if (text == null || text.getText() == null) - return false; - - //noinspection RedundantIfStatement - if (checkPosition && text.getPosition() == null) - return false; - - return true; - } - - protected class OrderedText implements OrderedRenderable, Comparable - { - GeographicText text; - Vec4 point; - double eyeDistance; - - OrderedText(GeographicText text, Vec4 point, double eyeDistance) - { - this.text = text; - this.point = point; - this.eyeDistance = eyeDistance; - } - - // When overlapping text are culled we want to sort them front to back by priority. - public int compareTo(OrderedText t) - { - if (t.text.getPriority() - this.text.getPriority() == 0) - { - return (int) (this.eyeDistance - t.eyeDistance); - } - else - return (int) (t.text.getPriority() - this.text.getPriority()); - } - - public double getDistanceFromEye() - { - return this.eyeDistance; - } - - private GeographicTextRenderer getRenderer() - { - return GeographicTextRenderer.this; - } - - public void render(DrawContext dc) - { - GeographicTextRenderer.this.beginRendering(dc); - try - { - if (cullText) - { - ArrayList textList = new ArrayList(); - textList.add(this); - - // Draw as many as we can in a batch to save ogl state switching. - Object nextItem = dc.peekOrderedRenderables(); - while (nextItem != null && nextItem instanceof OrderedText) - { - OrderedText ot = (OrderedText) nextItem; - if (ot.getRenderer() != GeographicTextRenderer.this) - break; - - textList.add(ot); - dc.pollOrderedRenderables(); // take it off the queue - nextItem = dc.peekOrderedRenderables(); - } - - Collections.sort(textList); // sort for rendering priority then front to back - - ArrayList textBounds = new ArrayList(); - for (OrderedText ot : textList) - { - double[] scaleAndOpacity = GeographicTextRenderer.this.computeDistanceScaleAndOpacity(dc, ot); - Rectangle2D newBounds = GeographicTextRenderer.this.computeTextBounds(dc, ot, - scaleAndOpacity[0]); - if (newBounds == null) - continue; - - boolean overlap = false; - newBounds = GeographicTextRenderer.this.computeExpandedBounds(newBounds, cullTextMargin); - for (Rectangle2D rect : textBounds) - { - if (rect.intersects(newBounds)) - overlap = true; - } - - if (!overlap) - { - textBounds.add(newBounds); - GeographicTextRenderer.this.drawText(dc, ot, scaleAndOpacity[0], scaleAndOpacity[1]); - } - } - } - else //just draw each label - { - double[] scaleAndOpacity = GeographicTextRenderer.this.computeDistanceScaleAndOpacity(dc, this); - GeographicTextRenderer.this.drawText(dc, this, scaleAndOpacity[0], scaleAndOpacity[1]); - // Draw as many as we can in a batch to save ogl state switching. - Object nextItem = dc.peekOrderedRenderables(); - while (nextItem != null && nextItem instanceof OrderedText) - { - OrderedText ot = (OrderedText) nextItem; - if (ot.getRenderer() != GeographicTextRenderer.this) - break; - - scaleAndOpacity = GeographicTextRenderer.this.computeDistanceScaleAndOpacity(dc, ot); - GeographicTextRenderer.this.drawText(dc, ot, scaleAndOpacity[0], scaleAndOpacity[1]); - dc.pollOrderedRenderables(); // take it off the queue - nextItem = dc.peekOrderedRenderables(); - } - } - } - catch (WWRuntimeException e) - { - Logging.logger().log(java.util.logging.Level.SEVERE, "generic.ExceptionWhileRenderingText", e); - } - catch (Exception e) - { - Logging.logger().log(java.util.logging.Level.SEVERE, "generic.ExceptionWhileRenderingText", e); - } - finally - { - GeographicTextRenderer.this.endRendering(dc); - } - } - - public void pick(DrawContext dc, java.awt.Point pickPoint) - { - } - } - - protected Rectangle2D computeTextBounds(DrawContext dc, OrderedText uText, double scale) throws Exception - { - GeographicText geographicText = uText.text; - - final CharSequence charSequence = geographicText.getText(); - if (charSequence == null) - return null; - - final Vec4 screenPoint = dc.getView().project(uText.point); - if (screenPoint == null) - return null; - - Font font = geographicText.getFont(); - if (font == null) - font = DEFAULT_FONT; - - try - { - TextRenderer textRenderer = OGLTextRenderer.getOrCreateTextRenderer(dc.getTextRendererCache(), font); - if (textRenderer != this.lastTextRenderer) - { - if (this.lastTextRenderer != null) - this.lastTextRenderer.end3DRendering(); - textRenderer.begin3DRendering(); - this.lastTextRenderer = textRenderer; - } - - Rectangle2D textBound = textRenderer.getBounds(charSequence); - double x = screenPoint.x - textBound.getWidth() / 2d; - Rectangle2D bounds = new Rectangle2D.Float(); - bounds.setRect(x, screenPoint.y, textBound.getWidth(), textBound.getHeight()); - - return computeScaledBounds(bounds, scale); - } - catch (Exception e) - { - handleTextRendererExceptions(e); - return null; - } - } - - protected Rectangle2D computeScaledBounds(Rectangle2D bounds, double scale) - { - if (scale == 1) - return bounds; - - // Scale rectangle from bottom center - double halfWidth = bounds.getWidth() / 2; - bounds.setRect(bounds.getX() + halfWidth - halfWidth * scale, bounds.getY(), - bounds.getWidth() * scale, bounds.getHeight() * scale); - return bounds; - } - - protected Rectangle2D computeExpandedBounds(Rectangle2D bounds, int margin) - { - if (margin == 0) - return bounds; - - // Add margin around rectangle - bounds.setRect(bounds.getX() - margin, bounds.getY() - margin, - bounds.getWidth() + margin * 2, bounds.getHeight() + margin * 2); - return bounds; - } - - @SuppressWarnings({"UnusedDeclaration"}) - protected double[] computeDistanceScaleAndOpacity(DrawContext dc, OrderedText ot) - { - if (!this.isDistanceScaling) - return new double[] {1, 1}; - - // Determine scale and opacity factors based on distance from eye vs the distance to the look at point. - double lookAtDistance = this.lookAtDistance; - double eyeDistance = ot.getDistanceFromEye(); - double distanceFactor = Math.sqrt(lookAtDistance / eyeDistance); - double scale = WWMath.clamp(distanceFactor, - this.getDistanceMinScale(), this.getDistanceMaxScale()); - double opacity = WWMath.clamp(distanceFactor, - this.getDistanceMinOpacity(), 1); - - return new double[] {scale, opacity}; - } - - protected double computeLookAtDistance(DrawContext dc) - { - View view = dc.getView(); - - // Get point in the middle of the screen - // TODO: Get a point on the surface rather then the geoid - Position groundPos = view.computePositionFromScreenPoint( - view.getViewport().getCenterX(), view.getViewport().getCenterY()); - - // Update look at distance if center point found - if (groundPos != null) - { - // Compute distance from eye to the position in the middle of the screen - this.lookAtDistance = view.getEyePoint().distanceTo3(dc.getGlobe().computePointFromPosition(groundPos)); - } - - return this.lookAtDistance; - } - - protected void beginRendering(DrawContext dc) - { - GL2 gl = dc.getGL().getGL2(); // GL initialization checks for GL2 compatibility. - int attribBits = - GL2.GL_ENABLE_BIT // for enable/disable changes - | GL2.GL_COLOR_BUFFER_BIT // for alpha test func and ref, and blend - | GL2.GL_CURRENT_BIT // for current color - | GL2.GL_DEPTH_BUFFER_BIT // for depth test, depth func, and depth mask - | GL2.GL_TRANSFORM_BIT // for modelview and perspective - | GL2.GL_VIEWPORT_BIT; // for depth range - gl.glPushAttrib(attribBits); - - gl.glMatrixMode(GL2.GL_PROJECTION); - gl.glPushMatrix(); - gl.glLoadIdentity(); - glu.gluOrtho2D(0, dc.getView().getViewport().width, 0, dc.getView().getViewport().height); - gl.glMatrixMode(GL2.GL_MODELVIEW); - gl.glPushMatrix(); - gl.glLoadIdentity(); - gl.glMatrixMode(GL2.GL_TEXTURE); - gl.glPushMatrix(); - gl.glLoadIdentity(); - // Set model view as current matrix mode - gl.glMatrixMode(GL2.GL_MODELVIEW); - - // 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(GL2.GL_ALPHA_TEST); - gl.glAlphaFunc(GL2.GL_GREATER, 0.001f); - - // Cache distance scaling values - this.isDistanceScaling = this.getDistanceMinScale() != 1 || this.getDistanceMaxScale() != 1 - || this.distanceMinOpacity != 1; - this.computeLookAtDistance(dc); - } - - protected void endRendering(DrawContext dc) - { - if (this.lastTextRenderer != null) - { - this.lastTextRenderer.end3DRendering(); - this.lastTextRenderer = null; - } - - GL2 gl = dc.getGL().getGL2(); // GL initialization checks for GL2 compatibility. - - gl.glMatrixMode(GL2.GL_PROJECTION); - gl.glPopMatrix(); - gl.glMatrixMode(GL2.GL_MODELVIEW); - gl.glPopMatrix(); - gl.glMatrixMode(GL2.GL_TEXTURE); - gl.glPopMatrix(); - - gl.glPopAttrib(); - } - - protected Vec4 drawText(DrawContext dc, OrderedText uText, double scale, double opacity) throws Exception - { - if (uText.point == null) - { - String msg = Logging.getMessage("nullValue.PointIsNull"); - Logging.logger().fine(msg); - return null; - } - - GeographicText geographicText = uText.text; - GL2 gl = dc.getGL().getGL2(); // GL initialization checks for GL2 compatibility. - - final CharSequence charSequence = geographicText.getText(); - if (charSequence == null) - return null; - - final Vec4 screenPoint = dc.getView().project(uText.point); - if (screenPoint == null) - return null; - - Font font = geographicText.getFont(); - if (font == null) - font = DEFAULT_FONT; - - try - { - TextRenderer textRenderer = OGLTextRenderer.getOrCreateTextRenderer(dc.getTextRendererCache(), font); - if (textRenderer != this.lastTextRenderer) - { - if (this.lastTextRenderer != null) - this.lastTextRenderer.end3DRendering(); - textRenderer.begin3DRendering(); - this.lastTextRenderer = textRenderer; - } - - this.setDepthFunc(dc, uText, screenPoint); - - Rectangle2D textBounds = textRenderer.getBounds( - charSequence);//note:may already be calculated during culling - textBounds = this.computeScaledBounds(textBounds, scale); - Point.Float drawPoint = computeDrawPoint(dc, textBounds, screenPoint); - - if (drawPoint != null) - { - if (scale != 1d) - { - gl.glScaled(scale, scale, 1d); - drawPoint.setLocation(drawPoint.x / (float) scale, drawPoint.y / (float) scale); - } - - Color color = geographicText.getColor(); - if (color == null) - color = DEFAULT_COLOR; - color = this.applyOpacity(color, opacity); - - Color background = geographicText.getBackgroundColor(); - if (background != null) - { - background = this.applyOpacity(background, opacity); - textRenderer.setColor(background); - if (this.effect.equals(AVKey.TEXT_EFFECT_SHADOW)) - { - textRenderer.draw3D(charSequence, drawPoint.x + 1, drawPoint.y - 1, 0, 1); - } - else if (this.effect.equals(AVKey.TEXT_EFFECT_OUTLINE)) - { - textRenderer.draw3D(charSequence, drawPoint.x + 1, drawPoint.y - 1, 0, 1); - textRenderer.draw3D(charSequence, drawPoint.x + 1, drawPoint.y + 1, 0, 1); - textRenderer.draw3D(charSequence, drawPoint.x - 1, drawPoint.y - 1, 0, 1); - textRenderer.draw3D(charSequence, drawPoint.x - 1, drawPoint.y + 1, 0, 1); - } - } - - textRenderer.setColor(color); - textRenderer.draw3D(charSequence, drawPoint.x, drawPoint.y, 0, 1); - textRenderer.flush(); - - if (scale != 1d) - gl.glLoadIdentity(); - } - } - catch (Exception e) - { - handleTextRendererExceptions(e); - } - - return screenPoint; - } - - protected Color applyOpacity(Color color, double opacity) - { - if (opacity >= 1) - return color; - - float[] compArray = color.getRGBComponents(null); - return new Color(compArray[0], compArray[1], compArray[2], compArray[3] * (float) opacity); - } - - private void handleTextRendererExceptions(Exception e) throws Exception - { - if (e instanceof IOException) - { - if (!this.hasJOGLv111Bug) - { - // This is likely a known JOGL 1.1.1 bug - see AMZN-287 or 343 - // Log once and then ignore. - Logging.logger().log(java.util.logging.Level.SEVERE, "generic.ExceptionWhileRenderingText", e); - this.hasJOGLv111Bug = true; - } - } - else - { - throw e; - } - } - - /** - * Computes the final draw point for the given rectangle lower left corner and target screen point. If the returned - * point is null the text will not be drawn. - * - * @param dc the current {@link DrawContext} - * @param rect the text rectangle to draw. - * @param screenPoint the projected screen point the text relates to. - * - * @return the final draw point for the given rectangle lower left corner or null. - */ - @SuppressWarnings({"UnusedDeclaration"}) - protected Point.Float computeDrawPoint(DrawContext dc, Rectangle2D rect, Vec4 screenPoint) - { - return new Point.Float((float) (screenPoint.x - rect.getWidth() / 2d), (float) (screenPoint.y)); - } - - @SuppressWarnings({"UnusedDeclaration"}) - protected void setDepthFunc(DrawContext dc, OrderedText uText, Vec4 screenPoint) - { - GL gl = dc.getGL(); - - //if (uText.text.isAlwaysOnTop()) - //{ - // gl.glDepthFunc(GL.GL_ALWAYS); - // return; - //} - - Position eyePos = dc.getView().getEyePosition(); - if (eyePos == null) - { - gl.glDepthFunc(GL.GL_ALWAYS); - return; - } - - double altitude = eyePos.getElevation(); - if (altitude < (dc.getGlobe().getMaxElevation() * dc.getVerticalExaggeration())) - { - double depth = screenPoint.z - (8d * 0.00048875809d); - depth = depth < 0d ? 0d : (depth > 1d ? 1d : depth); - gl.glDepthFunc(GL.GL_LESS); - gl.glDepthRange(depth, depth); - } - //else if (screenPoint.z >= 1d) - //{ - // gl.glDepthFunc(GL.GL_EQUAL); - // gl.glDepthRange(1d, 1d); - //} - else - { - gl.glDepthFunc(GL.GL_ALWAYS); - } - } -} + String getEffect(); + + void setEffect(String effect); + + void render(DrawContext dc, Iterable textIterable); +} \ No newline at end of file diff --git a/src/gov/nasa/worldwind/render/UserFacingText.java b/src/gov/nasa/worldwind/render/UserFacingText.java index 314c15d773..2cf28fd785 100644 --- a/src/gov/nasa/worldwind/render/UserFacingText.java +++ b/src/gov/nasa/worldwind/render/UserFacingText.java @@ -5,6 +5,7 @@ */ package gov.nasa.worldwind.render; +import gov.nasa.worldwind.avlist.AVKey; import gov.nasa.worldwind.geom.Position; import gov.nasa.worldwind.util.Logging; @@ -23,6 +24,7 @@ public class UserFacingText implements GeographicText private Color textBackgroundColor; // Can be null to indicate no background color. private boolean isVisible = true; double priority; //used for label culling + protected Offset offset = new Offset(0.0, 0.0, AVKey.FRACTION, AVKey.FRACTION); public UserFacingText(CharSequence text, Position textPosition) { @@ -63,6 +65,16 @@ public void setPriority(double priority) { this.priority = priority; } + + public Offset getOffset() + { + return this.offset; + } + + public void setOffset(Offset offset) + { + this.offset = offset; + } public Position getPosition() { diff --git a/src/gov/nasa/worldwindx/examples/util/OpenStreetMapShapefileLoader.java b/src/gov/nasa/worldwindx/examples/util/OpenStreetMapShapefileLoader.java index 179c49732d..978d9c521a 100644 --- a/src/gov/nasa/worldwindx/examples/util/OpenStreetMapShapefileLoader.java +++ b/src/gov/nasa/worldwindx/examples/util/OpenStreetMapShapefileLoader.java @@ -239,7 +239,7 @@ public OSMShapes(Color color, double scale, double labelMaxAltitude) protected static class TextAndShapesLayer extends RenderableLayer { protected ArrayList labels = new ArrayList(); - protected GeographicTextRenderer textRenderer = new GeographicTextRenderer(); + protected BasicGeographicTextRenderer textRenderer = new BasicGeographicTextRenderer(); public TextAndShapesLayer() {