/** * A label drawn as part of a tactical graphic. The label is drawn at constant screen size. The * label can include multiple lines of text, and can optionally be kept aligned with features on the * globe. To align a label with the globe specify an {@link * #setOrientationPosition(gov.nasa.worldwind.geom.Position) orientationPosition} for the label. The * label will be drawn along a line connecting the label's position to the orientation position. * * @author pabercrombie * @version $Id$ */ public class TacticalGraphicLabel implements OrderedRenderable { /** Default font. */ public static final Font DEFAULT_FONT = Font.decode("Arial-BOLD-16"); /** * Default offset. The default offset aligns the label horizontal with the text alignment * position, and centers the label vertically. For example, if the text alignment is <code> * AVKey.LEFT</code>, then the left edge of the text will be aligned with the geographic position, * and the label will be centered vertically. */ public static final Offset DEFAULT_OFFSET = new Offset(0d, -0.5d, AVKey.FRACTION, AVKey.FRACTION); /** Default insets around the label. */ public static final Insets DEFAULT_INSETS = new Insets(5, 5, 5, 5); /** Default interior opacity. */ public static final double DEFAULT_INTERIOR_OPACITY = 0.7; /** Default text effect (shadow). */ public static final String DEFAULT_TEXT_EFFECT = AVKey.TEXT_EFFECT_SHADOW; /** Text split into separate lines. */ protected String[] lines; /** The label's geographic position. */ protected Position position; /** Offset from the geographic position at which to draw the label. */ protected Offset offset = DEFAULT_OFFSET; /** Text alignment for multi-line labels. */ protected String textAlign = AVKey.LEFT; /** The label is drawn along a line from the label position to the orientation position. */ protected Position orientationPosition; /** Material used to draw the label. */ protected Material material = Material.BLACK; /** Opacity of the text, as a value between 0 and 1. */ protected double opacity = 1.0; protected double interiorOpacity = DEFAULT_INTERIOR_OPACITY; /** Font used to draw the label. */ protected Font font = DEFAULT_FONT; /** Space (in pixels) between lines in a multi-line label. */ protected int lineSpacing = 5; // TODO compute default based on font size /** * Effect applied to the text. May be {@link AVKey#TEXT_EFFECT_SHADOW} or {@link * AVKey#TEXT_EFFECT_NONE}. */ protected String effect = DEFAULT_TEXT_EFFECT; /** * Insets that separate the text from its frame. Only applies when the text interior is rendered. */ protected Insets insets = DEFAULT_INSETS; /** Indicates whether or not to draw the label interior. */ protected boolean drawInterior; /** Indicates whether or not batch rendering is enabled. */ protected boolean enableBatchRendering = false; /** Indicates whether or not batch picking is enabled. */ protected boolean enableBatchPicking = true; /** Indicates an object that represents the label during picking. */ protected Object delegateOwner; // Computed each frame protected long frameTimeStamp = -1L; /** Geographic position in cartesian coordinates. */ protected Vec4 placePoint; /** Location of the place point projected onto the screen. */ protected Vec4 screenPlacePoint; /** * Location of the upper left corner of the text measured from the lower left corner of the * viewport. This point in OGL coordinates. */ protected Point screenPoint; /** * Rotation applied to the label. This is computed each frame based on the orientation position. */ protected Angle rotation; /** * Height of a line of text, computed in {@link * #computeBoundsIfNeeded(gov.nasa.worldwind.render.DrawContext)}. */ protected int lineHeight; /** Size of the label. */ protected Rectangle2D bounds; /** Cached bounds for each line of text. */ protected Rectangle2D[] lineBounds; /** Extent of the label on the screen. */ protected Rectangle screenExtent; /** Distance from the eye point to the label's geographic location. */ protected double eyeDistance; /** Stack handler used for beginDrawing/endDrawing state. */ protected OGLStackHandler BEogsh = new OGLStackHandler(); /** Support object used during picking. */ protected PickSupport pickSupport = new PickSupport(); /** Active layer. */ protected Layer pickLayer; /** Create a new empty label. */ public TacticalGraphicLabel() {} /** * Create a new label. * * @param text Label text. */ public TacticalGraphicLabel(String text) { this.setText(text); } /** * Indicates the text of this label. * * @return The label's text. */ public String getText() { if (this.lines != null) { StringBuilder sb = new StringBuilder(); for (int i = 0; i < this.lines.length - 1; i++) { sb.append(this.lines[i]).append("\n"); } sb.append(this.lines[this.lines.length - 1]); return sb.toString(); } return null; } /** * Specifies the text of this label. The text may include multiple lines, separated by newline * characters. * * @param text New text. */ public void setText(String text) { if (text != null) this.lines = text.split("\n"); else this.lines = null; this.bounds = null; // Need to recompute } /** * Indicates the label's position. The label is drawn at an offset from this position. * * @return The label's geographic position. * @see #getOffset() */ public Position getPosition() { return this.position; } /** * Indicates the label's geographic position. The label is drawn at an offset from this position. * * @param position New position. * @see #getOffset() */ public void setPosition(Position position) { this.position = position; // Label has moved, need to recompute screen extent. Explicitly set the extent to null so that // it will be // recomputed even if the application calls setPosition multiple times per frame. this.screenExtent = null; } /** * Indicates the current text alignment. Can be one of {@link AVKey#LEFT} (default), {@link * AVKey#CENTER} or {@link AVKey#RIGHT}. * * @return the current text alignment. */ public String getTextAlign() { return this.textAlign; } /** * Specifies the text alignment. Can be one of {@link AVKey#LEFT} (default), {@link AVKey#CENTER}, * or {@link AVKey#RIGHT}. * * @param textAlign New text alignment. */ public void setTextAlign(String textAlign) { if (textAlign == null) { String message = Logging.getMessage("nullValue.StringIsNull"); Logging.logger().severe(message); throw new IllegalArgumentException(message); } this.textAlign = textAlign; } /** * Indicates the offset from the geographic position at which to draw the label. See {@link * #setOffset(gov.nasa.worldwind.render.Offset) setOffset} for more information on how the offset * is interpreted. * * @return The offset at which to draw the label. */ public Offset getOffset() { return this.offset; } /** * Specifies the offset from the geographic position at which to draw the label. The default * offset aligns the label horizontal with the text alignment position, and centers the label * vertically. For example, if the text alignment is <code>AVKey.LEFT</code>., then the left edge * of the text will be aligned with the geographic position, and the label will be centered * vertically. * * <p>When the text is rotated a horizontal offset moves the text along the orientation line, and * a vertical offset moves the text perpendicular to the orientation line. * * @param offset The offset at which to draw the label. */ public void setOffset(Offset offset) { if (offset == null) { String message = Logging.getMessage("nullValue.OffsetIsNull"); Logging.logger().severe(message); throw new IllegalArgumentException(message); } this.offset = offset; } /** * Indicates the font used to draw the label. * * @return The label's font. */ public Font getFont() { return this.font; } /** * Specifies the font used to draw the label. * * @param font New font. */ public void setFont(Font font) { if (font == null) { String message = Logging.getMessage("nullValue.FontIsNull"); Logging.logger().severe(message); throw new IllegalArgumentException(message); } if (font != this.font) { this.font = font; this.bounds = null; // Need to recompute } } /** * Indicates the line spacing applied to multi-line labels. * * @return The space (in pixels) between lines of a multi-line label. */ public int getLineSpacing() { return lineSpacing; } /** * Specifies the line spacing applied to multi-line labels. * * @param lineSpacing New line spacing. */ public void setLineSpacing(int lineSpacing) { if (lineSpacing < 0) { String message = Logging.getMessage("generic.ArgumentOutOfRange"); Logging.logger().severe(message); throw new IllegalArgumentException(message); } this.lineSpacing = lineSpacing; } /** * Indicates the material used to draw the label. * * @return The label's material. */ public Material getMaterial() { return this.material; } /** * Specifies the material used to draw the label. * * @param material New material. */ public void setMaterial(Material material) { if (material == null) { String message = Logging.getMessage("nullValue.MaterialIsNull"); Logging.logger().severe(message); throw new IllegalArgumentException(message); } this.material = material; } /** * Indicates whether or not to draw a colored frame behind the label. * * @return <code>true</code> if the label's interior is drawn, otherwise <code>false</code>. * @see #setDrawInterior(boolean) */ public boolean isDrawInterior() { return this.drawInterior; } /** * Specifies whether or not to draw a colored frame behind the label. * * @param drawInterior <code>true</code> if the label's interior is drawn, otherwise <code>false * </code>. * @see #isDrawInterior() */ public void setDrawInterior(boolean drawInterior) { this.drawInterior = drawInterior; } /** * Indicates the opacity of the text as a floating-point value in the range 0.0 to 1.0. A value of * 1.0 specifies a completely opaque text, and 0.0 specifies a completely transparent text. Values * in between specify a partially transparent text. * * @return the opacity of the text as a floating-point value from 0.0 to 1.0. */ public double getOpacity() { return this.opacity; } /** * Specifies the opacity of the text as a floating-point value in the range 0.0 to 1.0. A value of * 1.0 specifies a completely opaque text, and 0.0 specifies a completely transparent text. Values * in between specify a partially transparent text. * * @param opacity the opacity of text as a floating-point value from 0.0 to 1.0. * @throws IllegalArgumentException if <code>opacity</code> is less than 0.0 or greater than 1.0. */ public void setOpacity(double opacity) { if (opacity < 0 || opacity > 1) { String message = Logging.getMessage("generic.OpacityOutOfRange", opacity); Logging.logger().severe(message); throw new IllegalArgumentException(message); } this.opacity = opacity; } /** * Indicates the opacity of label's interior as a floating-point value in the range 0.0 to 1.0. A * value of 1.0 specifies a completely opaque interior, and 0.0 specifies a completely transparent * interior. Values in between specify a partially transparent interior. * * @return the opacity of the interior as a floating-point value from 0.0 to 1.0. */ public double getInteriorOpacity() { return this.interiorOpacity; } /** * Specifies the opacity of the label's interior as a floating-point value in the range 0.0 to * 1.0. A value of 1.0 specifies a completely opaque interior, and 0.0 specifies a completely * transparent interior. Values in between specify a partially transparent interior. * * @param interiorOpacity the opacity of label's interior as a floating-point value from 0.0 to * 1.0. * @throws IllegalArgumentException if <code>opacity</code> is less than 0.0 or greater than 1.0. */ public void setInteriorOpacity(double interiorOpacity) { if (opacity < 0 || opacity > 1) { String message = Logging.getMessage("generic.OpacityOutOfRange", opacity); Logging.logger().severe(message); throw new IllegalArgumentException(message); } this.interiorOpacity = interiorOpacity; } /** * Indicates the orientation position. The label oriented on a line drawn from the label's * position to the orientation position. * * @return Position used to orient the label. May be null. */ public Position getOrientationPosition() { return this.orientationPosition; } /** * Specifies the orientation position. The label is oriented on a line drawn from the label's * position to the orientation position. If the orientation position is null then the label is * drawn with no rotation. * * @param orientationPosition Draw label oriented toward this position. */ public void setOrientationPosition(Position orientationPosition) { this.orientationPosition = orientationPosition; } /** * Indicates the amount of space between the label's content and its frame, in pixels. * * @return the padding between the label's content and its frame, in pixels. * @see #setInsets(java.awt.Insets) */ public Insets getInsets() { return this.insets; } /** * Specifies the amount of space (in pixels) between the label's content and the edges of the * label's frame. * * @param insets the desired padding between the label's content and its frame, in pixels. * @throws IllegalArgumentException if <code>insets</code> is <code>null</code>. * @see #getInsets() */ public void setInsets(Insets insets) { if (insets == null) { String message = Logging.getMessage("nullValue.InsetsIsNull"); Logging.logger().severe(message); throw new IllegalArgumentException(message); } this.insets = insets; } /** * Indicates an effect used to decorate the text. Can be one of {@link AVKey#TEXT_EFFECT_SHADOW} * (default), or {@link AVKey#TEXT_EFFECT_NONE}. * * @return the effect used for text rendering */ public String getEffect() { return this.effect; } /** * Specifies an effect used to decorate the text. Can be one of {@link AVKey#TEXT_EFFECT_SHADOW} * (default), or {@link AVKey#TEXT_EFFECT_NONE}. * * @param effect the effect to use for text rendering */ public void setEffect(String effect) { if (effect == null) { String message = Logging.getMessage("nullValue.StringIsNull"); Logging.logger().severe(message); throw new IllegalArgumentException(message); } this.effect = effect; } /** * Returns the delegate owner of this label. If non-null, the returned object replaces the label * as the pickable object returned during picking. If null, the label itself is the pickable * object returned during picking. * * @return the object used as the pickable object returned during picking, or null to indicate the * the label is returned during picking. */ public Object getDelegateOwner() { return this.delegateOwner; } /** * Specifies the delegate owner of this label. If non-null, the delegate owner replaces the label * as the pickable object returned during picking. If null, the label itself is the pickable * object returned during picking. * * @param owner the object to use as the pickable object returned during picking, or null to * return the label. */ public void setDelegateOwner(Object owner) { this.delegateOwner = owner; } /** * Indicates whether batch picking is enabled. * * @return true if batch rendering is enabled, otherwise false. * @see #setEnableBatchPicking(boolean). */ public boolean isEnableBatchPicking() { return this.enableBatchPicking; } /** * Specifies whether adjacent Labels in the ordered renderable list may be pick-tested together if * they are contained in the same layer. This increases performance but allows only the top-most * of the label to be reported in a {@link gov.nasa.worldwind.event.SelectEvent} even if several * of the labels are at the pick position. * * <p>Batch rendering ({@link #setEnableBatchRendering(boolean)}) must be enabled in order for * batch picking to occur. * * @param enableBatchPicking true to enable batch rendering, otherwise false. */ public void setEnableBatchPicking(boolean enableBatchPicking) { this.enableBatchPicking = enableBatchPicking; } /** * Indicates whether batch rendering is enabled. * * @return true if batch rendering is enabled, otherwise false. * @see #setEnableBatchRendering(boolean). */ public boolean isEnableBatchRendering() { return this.enableBatchRendering; } /** * Specifies whether adjacent Labels in the ordered renderable list may be rendered together if * they are contained in the same layer. This increases performance and there is seldom a reason * to disable it. * * @param enableBatchRendering true to enable batch rendering, otherwise false. */ public void setEnableBatchRendering(boolean enableBatchRendering) { this.enableBatchRendering = enableBatchRendering; } /** * Get the label bounding {@link java.awt.Rectangle} using OGL coordinates - bottom-left corner x * and y relative to the {@link gov.nasa.worldwind.WorldWindow} bottom-left corner. If the label * is rotated then the returned rectangle is the bounding rectangle of the rotated label. * * @param dc the current DrawContext. * @return the label bounding {@link java.awt.Rectangle} using OGL viewport coordinates. * @throws IllegalArgumentException if <code>dc</code> is null. */ public Rectangle getBounds(DrawContext dc) { if (dc == null) { String message = Logging.getMessage("nullValue.DrawContextIsNull"); Logging.logger().severe(message); throw new IllegalArgumentException(message); } this.computeGeometryIfNeeded(dc); return this.screenExtent; } /** * Compute label geometry, if it has not already been computed this frame, or if the label * position has changed since the extent was last computed. * * @param dc Current geometry. */ protected void computeGeometryIfNeeded(DrawContext dc) { // Re-use rendering state values already calculated this frame. If the screenExtent is null, // recompute even if // the timestamp is the same. This prevents using a stale position if the application calls // setPosition and // getBounds multiple times before the label is rendered. long timeStamp = dc.getFrameTimeStamp(); if (timeStamp != this.frameTimeStamp || this.screenExtent == null) { this.computeGeometry(dc); this.frameTimeStamp = timeStamp; } } /** * Compute the bounds of the text, if necessary. * * @param dc the current DrawContext. */ protected void computeBoundsIfNeeded(DrawContext dc) { // Do not compute bounds if they are available. Computing text bounds is expensive, so only do // this // calculation if necessary. if (this.bounds != null) return; TextRenderer textRenderer = OGLTextRenderer.getOrCreateTextRenderer(dc.getTextRendererCache(), this.getFont()); int width = 0; int maxLineHeight = 0; this.lineBounds = new Rectangle2D[this.lines.length]; for (int i = 0; i < this.lines.length; i++) { Rectangle2D lineBounds = textRenderer.getBounds(lines[i]); width = (int) Math.max(lineBounds.getWidth(), width); double thisLineHeight = Math.abs(lineBounds.getY()); maxLineHeight = (int) Math.max(thisLineHeight, maxLineHeight); this.lineBounds[i] = lineBounds; } this.lineHeight = maxLineHeight; // Compute final height using maxLineHeight and number of lines this.bounds = new Rectangle( this.lines.length, maxLineHeight, width, this.lines.length * maxLineHeight + this.lines.length * this.lineSpacing); } /** * Compute the label's screen position from its geographic position. * * @param dc Current draw context. */ protected void computeGeometry(DrawContext dc) { // Project the label position onto the viewport Position pos = this.getPosition(); if (pos == null) return; this.placePoint = dc.computeTerrainPoint(pos.getLatitude(), pos.getLongitude(), 0); this.screenPlacePoint = dc.getView().project(this.placePoint); this.eyeDistance = this.placePoint.distanceTo3(dc.getView().getEyePoint()); boolean orientationReversed = false; if (this.orientationPosition != null) { // Project the orientation point onto the screen Vec4 orientationPlacePoint = dc.computeTerrainPoint( this.orientationPosition.getLatitude(), this.orientationPosition.getLongitude(), 0); Vec4 orientationScreenPoint = dc.getView().project(orientationPlacePoint); this.rotation = this.computeRotation(this.screenPlacePoint, orientationScreenPoint); // The orientation is reversed if the orientation point falls to the right of the screen // point. Text is // never drawn upside down, so when the orientation is reversed the text flips vertically to // keep the text // right side up. orientationReversed = (orientationScreenPoint.x <= this.screenPlacePoint.x); } this.computeBoundsIfNeeded(dc); Offset offset = this.getOffset(); Point2D offsetPoint = offset.computeOffset(this.bounds.getWidth(), this.bounds.getHeight(), null, null); // If a rotation is applied to the text, then rotate the offset as well. An offset in the x // direction // will move the text along the orientation line, and a offset in the y direction will move the // text // perpendicular to the orientation line. if (this.rotation != null) { double dy = offsetPoint.getY(); // If the orientation is reversed we need to adjust the vertical offset to compensate for the // flipped // text. For example, if the offset normally aligns the top of the text with the place point // then without // this adjustment the bottom of the text would align with the place point when the // orientation is // reversed. if (orientationReversed) { dy = -(dy + this.bounds.getHeight()); } Vec4 pOffset = new Vec4(offsetPoint.getX(), dy); Matrix rot = Matrix.fromRotationZ(this.rotation.multiply(-1)); pOffset = pOffset.transformBy3(rot); offsetPoint = new Point((int) pOffset.getX(), (int) pOffset.getY()); } int x = (int) (this.screenPlacePoint.x + offsetPoint.getX()); int y = (int) (this.screenPlacePoint.y - offsetPoint.getY()); this.screenPoint = new Point(x, y); this.screenExtent = this.computeTextExtent(x, y, this.rotation); } /** * Determine if this label intersects the view or pick frustum. * * @param dc Current draw context. * @return True if this label intersects the active frustum (view or pick). Otherwise false. */ protected boolean intersectsFrustum(DrawContext dc) { View view = dc.getView(); Frustum frustum = view.getFrustumInModelCoordinates(); // Test the label's model coordinate point against the near and far clipping planes. if (this.placePoint != null && (frustum.getNear().distanceTo(this.placePoint) < 0 || frustum.getFar().distanceTo(this.placePoint) < 0)) { return false; } if (dc.isPickingMode()) return dc.getPickFrustums().intersectsAny(this.screenExtent); else return view.getViewport().intersects(this.screenExtent); } /** * Compute the amount of rotation to apply to a label in order to keep it oriented toward its * orientation position. * * @param screenPoint Geographic position of the text, projected onto the screen. * @param orientationScreenPoint Orientation position, projected onto the screen. * @return The rotation angle to apply when drawing the label. */ protected Angle computeRotation(Vec4 screenPoint, Vec4 orientationScreenPoint) { // Determine delta between the orientation position and the label position double deltaX = screenPoint.x - orientationScreenPoint.x; double deltaY = screenPoint.y - orientationScreenPoint.y; if (deltaX != 0) { double angle = Math.atan(deltaY / deltaX); return Angle.fromRadians(angle); } else { return Angle.POS90; // Vertical label } } /** {@inheritDoc} */ public double getDistanceFromEye() { return this.eyeDistance; } /** {@inheritDoc} */ public void render(DrawContext dc) { // This render method is called three times during frame generation. It's first called as a // Renderable // during Renderable picking. It's called again during normal rendering. And it's called a third // time as an OrderedRenderable. The first two calls determine whether to add the label the // ordered renderable // list during pick and render. The third call just draws the ordered renderable. if (dc == null) { String msg = Logging.getMessage("nullValue.DrawContextIsNull"); Logging.logger().severe(msg); throw new IllegalArgumentException(msg); } if (dc.isOrderedRenderingMode()) this.drawOrderedRenderable(dc); else this.makeOrderedRenderable(dc); } /** {@inheritDoc} */ public void pick(DrawContext dc, Point pickPoint) { // This method is called only when ordered renderables are being drawn. // Arg checked within call to render. if (dc == null) { String msg = Logging.getMessage("nullValue.DrawContextIsNull"); Logging.logger().severe(msg); throw new IllegalArgumentException(msg); } this.pickSupport.clearPickList(); try { this.pickSupport.beginPicking(dc); this.render(dc); } finally { this.pickSupport.endPicking(dc); this.pickSupport.resolvePick(dc, pickPoint, this.pickLayer); } } /** * Draws the graphic as an ordered renderable. * * @param dc the current draw context. */ protected void makeOrderedRenderable(DrawContext dc) { if (this.lines == null || this.position == null) return; this.computeGeometryIfNeeded(dc); // Don't draw if beyond the horizon. double horizon = dc.getView().getHorizonDistance(); if (this.eyeDistance > horizon) return; if (this.intersectsFrustum(dc)) dc.addOrderedRenderable(this); if (dc.isPickingMode()) this.pickLayer = dc.getCurrentLayer(); } /** * Draws the graphic as an ordered renderable. * * @param dc the current draw context. */ protected void drawOrderedRenderable(DrawContext dc) { this.beginDrawing(dc); try { this.doDrawOrderedRenderable(dc, this.pickSupport); if (this.isEnableBatchRendering()) this.drawBatched(dc); } finally { this.endDrawing(dc); } } /** * Draw this label during ordered rendering. * * @param dc Current draw context. * @param pickSupport Support object used during picking. */ protected void doDrawOrderedRenderable(DrawContext dc, PickSupport pickSupport) { TextRenderer textRenderer = OGLTextRenderer.getOrCreateTextRenderer(dc.getTextRendererCache(), font); if (dc.isPickingMode()) { this.doPick(dc, pickSupport); } else { this.drawText(dc, textRenderer); } } /** * Establish the OpenGL state needed to draw text. * * @param dc the current draw context. */ protected void beginDrawing(DrawContext dc) { GL gl = dc.getGL(); int attrMask = GL.GL_DEPTH_BUFFER_BIT // for depth test, depth mask and depth func | GL.GL_TRANSFORM_BIT // for modelview and perspective | GL.GL_VIEWPORT_BIT // for depth range | GL.GL_CURRENT_BIT // for current color | GL.GL_COLOR_BUFFER_BIT // for alpha test func and ref, and blend | GL.GL_DEPTH_BUFFER_BIT // for depth func | GL.GL_ENABLE_BIT; // for enable/disable changes this.BEogsh.pushAttrib(gl, attrMask); if (!dc.isPickingMode()) { gl.glEnable(GL.GL_BLEND); OGLUtil.applyBlending(gl, false); } // Do not depth buffer the label. (Labels beyond the horizon are culled above.) gl.glDisable(GL.GL_DEPTH_TEST); gl.glDepthMask(false); // The image is drawn using a parallel projection. this.BEogsh.pushProjectionIdentity(gl); gl.glOrtho( 0d, dc.getView().getViewport().width, 0d, dc.getView().getViewport().height, -1d, 1d); this.BEogsh.pushModelviewIdentity(gl); } /** * Pop the state set in beginDrawing. * * @param dc the current draw context. */ protected void endDrawing(DrawContext dc) { this.BEogsh.pop(dc.getGL()); } /** * Draw labels for picking. * * @param dc Current draw context. * @param pickSupport the PickSupport instance to be used. */ protected void doPick(DrawContext dc, PickSupport pickSupport) { GL gl = dc.getGL(); Angle heading = this.rotation; double headingDegrees; if (heading != null) headingDegrees = heading.degrees; else headingDegrees = 0; int x = this.screenPoint.x; int y = this.screenPoint.y; boolean matrixPushed = false; try { if (headingDegrees != 0) { gl.glPushMatrix(); matrixPushed = true; gl.glTranslated(x, y, 0); gl.glRotated(headingDegrees, 0, 0, 1); gl.glTranslated(-x, -y, 0); } for (int i = 0; i < this.lines.length; i++) { Rectangle2D bounds = this.lineBounds[i]; double width = bounds.getWidth(); double height = bounds.getHeight(); x = this.screenPoint.x; if (this.textAlign.equals(AVKey.CENTER)) x = x - (int) (width / 2.0); else if (this.textAlign.equals(AVKey.RIGHT)) x = x - (int) width; y -= this.lineHeight; Color color = dc.getUniquePickColor(); int colorCode = color.getRGB(); PickedObject po = new PickedObject(colorCode, this.getPickedObject(), this.position, false); pickSupport.addPickableObject(po); // Draw line rectangle gl.glColor3ub((byte) color.getRed(), (byte) color.getGreen(), (byte) color.getBlue()); try { gl.glBegin(GL.GL_POLYGON); gl.glVertex3d(x, y, 0); gl.glVertex3d(x + width - 1, y, 0); gl.glVertex3d(x + width - 1, y + height - 1, 0); gl.glVertex3d(x, y + height - 1, 0); gl.glVertex3d(x, y, 0); } finally { gl.glEnd(); } y -= this.lineSpacing; } } finally { if (matrixPushed) { gl.glPopMatrix(); } } } /** * Draw the label's text. This method sets up the text renderer, and then calls {@link * #doDrawText(TextRenderer) doDrawText} to actually draw the text. * * @param dc Current draw context. * @param textRenderer Text renderer. */ protected void drawText(DrawContext dc, TextRenderer textRenderer) { GL gl = dc.getGL(); Angle heading = this.rotation; double headingDegrees; if (heading != null) headingDegrees = heading.degrees; else headingDegrees = 0; boolean matrixPushed = false; try { int x = this.screenPoint.x; int y = this.screenPoint.y; if (headingDegrees != 0) { gl.glPushMatrix(); matrixPushed = true; gl.glTranslated(x, y, 0); gl.glRotated(headingDegrees, 0, 0, 1); gl.glTranslated(-x, -y, 0); } if (this.isDrawInterior()) this.drawInterior(dc); textRenderer.begin3DRendering(); try { this.doDrawText(textRenderer); // Draw other labels that share the same text renderer configuration, if possible. if (this.isEnableBatchRendering()) this.drawBatchedText(dc, textRenderer); } finally { textRenderer.end3DRendering(); } } finally { if (matrixPushed) { gl.glPopMatrix(); } } } /** * Render the label interior as a filled rectangle. * * @param dc Current draw context. */ protected void drawInterior(DrawContext dc) { GL gl = dc.getGL(); double width = this.bounds.getWidth(); double height = this.bounds.getHeight(); int x = this.screenPoint.x; int y = this.screenPoint.y; // Adjust x to account for text alignment int xAligned = x; if (AVKey.CENTER.equals(textAlign)) xAligned = x - (int) (width / 2); else if (AVKey.RIGHT.equals(textAlign)) xAligned = x - (int) width; // We draw text top-down, so adjust y to compensate. int yAligned = (int) (y - height); // Apply insets Insets insets = this.getInsets(); xAligned -= insets.left; width = width + insets.left + insets.right; yAligned -= insets.bottom; height = height + insets.bottom + insets.top; if (!dc.isPickingMode()) { // Apply the frame background color and opacity if we're in normal rendering mode. Color color = this.computeBackgroundColor(this.getMaterial().getDiffuse()); gl.glColor4ub( (byte) color.getRed(), (byte) color.getGreen(), (byte) color.getBlue(), (byte) (this.interiorOpacity < 1 ? (int) (this.interiorOpacity * 255 + 0.5) : 255)); } try { // Draw a quad gl.glPushMatrix(); gl.glTranslated(xAligned, yAligned, 0); gl.glScaled(width, height, 1.0); dc.drawUnitQuad(); } finally { gl.glPopMatrix(); } } /** * Draw the label's text. This method assumes that the text renderer context has already been set * up. * * @param textRenderer renderer to use. */ protected void doDrawText(TextRenderer textRenderer) { Color color = this.material.getDiffuse(); Color backgroundColor = this.computeBackgroundColor(color); float opacity = (float) this.getOpacity(); int x = this.screenPoint.x; int y = this.screenPoint.y; float[] compArray = new float[3]; if (AVKey.TEXT_EFFECT_SHADOW.equals(this.effect) && backgroundColor != null) { backgroundColor.getRGBColorComponents(compArray); textRenderer.setColor(compArray[0], compArray[1], compArray[2], opacity); this.drawMultiLineText(textRenderer, x + 1, y - 1); } color.getRGBColorComponents(compArray); textRenderer.setColor(compArray[0], compArray[1], compArray[2], opacity); this.drawMultiLineText(textRenderer, x, y); } protected void drawMultiLineText(TextRenderer textRenderer, int x, int y) { if (this.lines == null) { String msg = Logging.getMessage("nullValue.StringIsNull"); Logging.logger().severe(msg); throw new IllegalArgumentException(msg); } for (int i = 0; i < this.lines.length; i++) { String line = this.lines[i]; Rectangle2D bounds = this.lineBounds[i]; int xAligned = x; if (this.textAlign.equals(AVKey.CENTER)) xAligned = x - (int) (bounds.getWidth() / 2); else if (this.textAlign.equals(AVKey.RIGHT)) xAligned = x - (int) (bounds.getWidth()); y -= this.lineHeight; textRenderer.draw3D(line, xAligned, y, 0, 1); y -= this.lineSpacing; } } /** * Draws this ordered renderable and all subsequent Label ordered renderables in the ordered * renderable list. This method differs from {@link * #drawBatchedText(gov.nasa.worldwind.render.DrawContext, TextRenderer) drawBatchedText} in that * this method re-initializes the text renderer to draw the next label, while {@code * drawBatchedText} re-uses the active text renderer context. That is, {@code drawBatchedText} * attempts to draw as many labels as possible that share same text renderer configuration as this * label, and this method attempts to draw as many labels as possible regardless of the text * renderer configuration of the subsequent labels. * * @param dc the current draw context. */ protected void drawBatched(DrawContext dc) { // Draw as many as we can in a batch to save ogl state switching. Object nextItem = dc.peekOrderedRenderables(); if (!dc.isPickingMode()) { while (nextItem != null && nextItem instanceof TacticalGraphicLabel) { TacticalGraphicLabel nextLabel = (TacticalGraphicLabel) nextItem; if (!nextLabel.isEnableBatchRendering()) break; dc.pollOrderedRenderables(); // take it off the queue nextLabel.doDrawOrderedRenderable(dc, this.pickSupport); nextItem = dc.peekOrderedRenderables(); } } else if (this.isEnableBatchPicking()) { while (nextItem != null && nextItem instanceof TacticalGraphicLabel) { TacticalGraphicLabel nextLabel = (TacticalGraphicLabel) nextItem; if (!nextLabel.isEnableBatchRendering() || !nextLabel.isEnableBatchPicking()) break; if (nextLabel.pickLayer != this.pickLayer) // batch pick only within a single layer break; dc.pollOrderedRenderables(); // take it off the queue nextLabel.doDrawOrderedRenderable(dc, this.pickSupport); nextItem = dc.peekOrderedRenderables(); } } } /** * Draws text for subsequent Label ordered renderables in the ordered renderable list. This method * is called after the text renderer has been set up (after beginRendering has been called), so * this method can only draw text for subsequent labels that use the same font and rotation as * this label. This method differs from {@link #drawBatched(gov.nasa.worldwind.render.DrawContext) * drawBatched} in that this method reuses the active text renderer context to draw as many labels * as possible without switching text renderer state. * * @param dc the current draw context. * @param textRenderer Text renderer used to draw the label. */ protected void drawBatchedText(DrawContext dc, TextRenderer textRenderer) { // Draw as many as we can in a batch to save ogl state switching. Object nextItem = dc.peekOrderedRenderables(); if (!dc.isPickingMode()) { while (nextItem != null && nextItem instanceof TacticalGraphicLabel) { TacticalGraphicLabel nextLabel = (TacticalGraphicLabel) nextItem; if (!nextLabel.isEnableBatchRendering()) break; boolean sameFont = this.font.equals(nextLabel.getFont()); boolean sameRotation = (this.rotation == null && nextLabel.rotation == null) || (this.rotation != null && this.rotation.equals(nextLabel.rotation)); boolean drawInterior = nextLabel.isDrawInterior(); // We've already set up the text renderer state, so we can can't change the font or text // rotation. // Also can't batch render if the next label needs an interior since that will require // tearing down the // text renderer context. if (!sameFont || !sameRotation || drawInterior) break; dc.pollOrderedRenderables(); // take it off the queue nextLabel.doDrawText(textRenderer); nextItem = dc.peekOrderedRenderables(); } } } /** * Indicates the object that represents this label during picking. * * @return If a delegate owner is set, returns the delegate owner. Otherwise returns this label. */ protected Object getPickedObject() { Object owner = this.getDelegateOwner(); return (owner != null) ? owner : this; } /** * Determine the screen rectangle covered by a label. The input coordinate identifies either the * top left, top center, or top right corner of the label, depending on the text alignment. If the * label is rotated to align with features on the surface then the extent will be the smallest * screen rectangle that completely encloses the rotated label. * * @param x X coordinate at which to draw the label. * @param y Y coordinate at which to draw the label. * @param rotation Label rotation. * @return The rectangle, in OGL screen coordinates (origin at bottom left corner), that is * covered by the label. */ protected Rectangle computeTextExtent(int x, int y, Angle rotation) { double width = this.bounds.getWidth(); double height = this.bounds.getHeight(); String textAlign = this.getTextAlign(); int xAligned = x; if (AVKey.CENTER.equals(textAlign)) xAligned = x - (int) (width / 2); else if (AVKey.RIGHT.equals(textAlign)) xAligned = x - (int) width; int yAligned = (int) (y - height); Rectangle screenRect = new Rectangle(xAligned, yAligned, (int) width, (int) height); // Compute bounds of the rotated rectangle, if there is a rotation angle. if (rotation != null && rotation.degrees != 0) { screenRect = this.computeRotatedScreenExtent(screenRect, x, y, rotation); } return screenRect; } /** * Compute the bounding screen extent of a rotated rectangle. * * @param rect Rectangle to rotate. * @param x X coordinate of the rotation point. * @param y Y coordinate of the rotation point. * @param rotation Rotation angle. * @return The smallest rectangle that completely contains {@code rect} when rotated by the * specified angle. */ protected Rectangle computeRotatedScreenExtent(Rectangle rect, int x, int y, Angle rotation) { Rectangle r = new Rectangle(rect); // Translate the rectangle to the rotation point. r.translate(-x, -y); // Compute corner points Vec4[] corners = { new Vec4(r.getMaxX(), r.getMaxY()), new Vec4(r.getMaxX(), r.getMinY()), new Vec4(r.getMinX(), r.getMaxY()), new Vec4(r.getMinX(), r.getMinY()) }; // Rotate the rectangle Matrix rotationMatrix = Matrix.fromRotationZ(rotation); for (int i = 0; i < corners.length; i++) { corners[i] = corners[i].transformBy3(rotationMatrix); } // Find the bounding rectangle of rotated points. int minX = Integer.MAX_VALUE; int minY = Integer.MAX_VALUE; int maxX = -Integer.MAX_VALUE; int maxY = -Integer.MAX_VALUE; for (Vec4 v : corners) { if (v.x > maxX) maxX = (int) v.x; if (v.x < minX) minX = (int) v.x; if (v.y > maxY) maxY = (int) v.y; if (v.y < minY) minY = (int) v.y; } // Set bounds and translate the rectangle back to where it started. r.setBounds(minX, minY, maxX - minX, maxY - minY); r.translate(x, y); return r; } /** * Compute a contrasting background color to draw the label's outline. * * @param color Label color. * @return A color that contrasts with {@code color}. */ protected Color computeBackgroundColor(Color color) { float[] colorArray = new float[4]; Color.RGBtoHSB(color.getRed(), color.getGreen(), color.getBlue(), colorArray); if (colorArray[2] > 0.5) return new Color(0, 0, 0, 0.7f); else return new Color(1, 1, 1, 0.7f); } }
/** * Renders a scalebar graphic in a screen corner. * * @author Patrick Murris * @version $Id: ScalebarLayer.java 12872 2009-12-09 15:26:15Z patrickmurris $ */ public class ScalebarLayer extends AbstractLayer { // Units constants public static final String UNIT_METRIC = "gov.nasa.worldwind.ScalebarLayer.Metric"; public static final String UNIT_IMPERIAL = "gov.nasa.worldwind.ScalebarLayer.Imperial"; // Display parameters - TODO: make configurable private Dimension size = new Dimension(150, 10); private Color color = Color.white; private int borderWidth = 20; private String position = AVKey.SOUTHEAST; private String resizeBehavior = AVKey.RESIZE_SHRINK_ONLY; private String unit = UNIT_METRIC; private Font defaultFont = Font.decode("Arial-PLAIN-12"); private double toViewportScale = 0.2; private PickSupport pickSupport = new PickSupport(); private Vec4 locationCenter = null; private Vec4 locationOffset = null; private double pixelSize; // Draw it as ordered with an eye distance of 0 so that it shows up in front of most other things. // TODO: Add general support for this common pattern. private OrderedIcon orderedImage = new OrderedIcon(); private class OrderedIcon implements OrderedRenderable { public double getDistanceFromEye() { return 0; } public void pick(DrawContext dc, Point pickPoint) { ScalebarLayer.this.draw(dc); } public void render(DrawContext dc) { ScalebarLayer.this.draw(dc); } } /** Renders a scalebar graphic in a screen corner */ public ScalebarLayer() { setPickEnabled(false); } // Public properties /** * Get the apparent pixel size in meter at the reference position. * * @return the apparent pixel size in meter at the reference position. */ public double getPixelSize() { return this.pixelSize; } /** * Get the scalebar graphic Dimension (in pixels) * * @return the scalebar graphic Dimension */ public Dimension getSize() { return this.size; } /** * Set the scalebar graphic Dimenion (in pixels) * * @param size the scalebar graphic Dimension */ public void setSize(Dimension size) { if (size == null) { String message = Logging.getMessage("nullValue.DimensionIsNull"); Logging.logger().severe(message); throw new IllegalArgumentException(message); } this.size = size; } /** * Get the scalebar color * * @return the scalebar Color */ public Color getColor() { return this.color; } /** * Set the scalbar Color * * @param color the scalebar Color */ public void setColor(Color color) { if (color == null) { String msg = Logging.getMessage("nullValue.ColorIsNull"); Logging.logger().severe(msg); throw new IllegalArgumentException(msg); } this.color = color; } /** * Returns the scalebar-to-viewport scale factor. * * @return the scalebar-to-viewport scale factor */ public double getToViewportScale() { return toViewportScale; } /** * Sets the scale factor applied to the viewport size to determine the displayed size of the * scalebar. This scale factor is used only when the layer's resize behavior is * AVKey.RESIZE_STRETCH or AVKey.RESIZE_SHRINK_ONLY. The scalebar's width is adjusted to occupy * the proportion of the viewport's width indicated by this factor. The scalebar's height is * adjusted to maintain the scalebar's Dimension aspect ratio. * * @param toViewportScale the scalebar to viewport scale factor */ public void setToViewportScale(double toViewportScale) { this.toViewportScale = toViewportScale; } public String getPosition() { return this.position; } /** * Sets the relative viewport location to display the scalebar. Can be one of AVKey.NORTHEAST, * AVKey.NORTHWEST, AVKey.SOUTHEAST (the default), or AVKey.SOUTHWEST. These indicate the corner * of the viewport. * * @param position the desired scalebar position */ public void setPosition(String position) { if (position == null) { String msg = Logging.getMessage("nullValue.PositionIsNull"); Logging.logger().severe(msg); throw new IllegalArgumentException(msg); } this.position = position; } /** * Returns the current scalebar center location. * * @return the current location center. May be null. */ public Vec4 getLocationCenter() { return locationCenter; } /** * Specifies the screen location of the scalebar center. May be null. If this value is non-null, * it overrides the position specified by #setPosition. The location is specified in pixels. The * origin is the window's lower left corner. Positive X values are to the right of the origin, * positive Y values are upwards from the origin. The final scalebar location will be affected by * the currently specified location offset if a non-null location offset has been specified (see * #setLocationOffset). * * @param locationCenter the scalebar center. May be null. * @see #setPosition, #setLocationOffset */ public void setLocationCenter(Vec4 locationCenter) { this.locationCenter = locationCenter; } /** * Returns the current location offset. See #setLocationOffset for a description of the offset and * its values. * * @return the location offset. Will be null if no offset has been specified. */ public Vec4 getLocationOffset() { return locationOffset; } /** * Specifies a placement offset from the scalebar's position on the screen. * * @param locationOffset the number of pixels to shift the scalebar from its specified screen * position. A positive X value shifts the image to the right. A positive Y value shifts the * image up. If null, no offset is applied. The default offset is null. * @see #setLocationCenter, #setPosition */ public void setLocationOffset(Vec4 locationOffset) { this.locationOffset = locationOffset; } /** * Returns the layer's resize behavior. * * @return the layer's resize behavior */ public String getResizeBehavior() { return resizeBehavior; } /** * Sets the behavior the layer uses to size the scalebar when the viewport size changes, typically * when the World Wind window is resized. If the value is AVKey.RESIZE_KEEP_FIXED_SIZE, the * scalebar size is kept to the size specified in its Dimension scaled by the layer's current icon * scale. If the value is AVKey.RESIZE_STRETCH, the scalebar is resized to have a constant size * relative to the current viewport size. If the viewport shrinks the scalebar size decreases; if * it expands then the scalebar enlarges. If the value is AVKey.RESIZE_SHRINK_ONLY (the default), * scalebar sizing behaves as for AVKey.RESIZE_STRETCH but it will not grow larger than the size * specified in its Dimension. * * @param resizeBehavior the desired resize behavior */ public void setResizeBehavior(String resizeBehavior) { this.resizeBehavior = resizeBehavior; } public int getBorderWidth() { return borderWidth; } /** * Sets the scalebar offset from the viewport border. * * @param borderWidth the number of pixels to offset the scalebar from the borders indicated by * {@link #setPosition(String)}. */ public void setBorderWidth(int borderWidth) { this.borderWidth = borderWidth; } public String getUnit() { return this.unit; } /** * Sets the unit the scalebar uses to display distances. Can be one of {@link #UNIT_METRIC} (the * default), or {@link #UNIT_IMPERIAL}. * * @param unit the desired unit */ public void setUnit(String unit) { this.unit = unit; } /** * Get the scalebar legend Fon * * @return the scalebar legend Font */ public Font getFont() { return this.defaultFont; } /** * Set the scalebar legend Fon * * @param font the scalebar legend Font */ public void setFont(Font font) { if (font == null) { String msg = Logging.getMessage("nullValue.FontIsNull"); Logging.logger().severe(msg); throw new IllegalArgumentException(msg); } this.defaultFont = font; } // Rendering @Override public void doRender(DrawContext dc) { dc.addOrderedRenderable(this.orderedImage); } @Override public void doPick(DrawContext dc, Point pickPoint) { // Delegate drawing to the ordered renderable list dc.addOrderedRenderable(this.orderedImage); } // Rendering public void draw(DrawContext dc) { GL gl = dc.getGL(); boolean attribsPushed = false; boolean modelviewPushed = false; boolean projectionPushed = false; try { gl.glPushAttrib( GL.GL_DEPTH_BUFFER_BIT | GL.GL_COLOR_BUFFER_BIT | GL.GL_ENABLE_BIT | GL.GL_TEXTURE_BIT | GL.GL_TRANSFORM_BIT | GL.GL_VIEWPORT_BIT | GL.GL_CURRENT_BIT); attribsPushed = true; gl.glDisable(GL.GL_TEXTURE_2D); // no textures gl.glEnable(GL.GL_BLEND); gl.glBlendFunc(GL.GL_SRC_ALPHA, GL.GL_ONE_MINUS_SRC_ALPHA); gl.glDisable(GL.GL_DEPTH_TEST); double width = this.size.width; double height = this.size.height; // Load a parallel projection with xy dimensions (viewportWidth, viewportHeight) // into the GL projection matrix. java.awt.Rectangle viewport = dc.getView().getViewport(); gl.glMatrixMode(javax.media.opengl.GL.GL_PROJECTION); gl.glPushMatrix(); projectionPushed = true; gl.glLoadIdentity(); double maxwh = width > height ? width : height; gl.glOrtho(0d, viewport.width, 0d, viewport.height, -0.6 * maxwh, 0.6 * maxwh); gl.glMatrixMode(GL.GL_MODELVIEW); gl.glPushMatrix(); modelviewPushed = true; gl.glLoadIdentity(); // Scale to a width x height space // located at the proper position on screen double scale = this.computeScale(viewport); Vec4 locationSW = this.computeLocation(viewport, scale); gl.glTranslated(locationSW.x(), locationSW.y(), locationSW.z()); gl.glScaled(scale, scale, 1); // Compute scale size in real world Position referencePosition = dc.getViewportCenterPosition(); if (referencePosition != null) { Vec4 groundTarget = dc.getGlobe().computePointFromPosition(referencePosition); Double distance = dc.getView().getEyePoint().distanceTo3(groundTarget); this.pixelSize = dc.getView().computePixelSizeAtDistance(distance); Double scaleSize = this.pixelSize * width * scale; // meter String unitLabel = "m"; if (this.unit.equals(UNIT_METRIC)) { if (scaleSize > 10000) { scaleSize /= 1000; unitLabel = "Km"; } } else if (this.unit.equals(UNIT_IMPERIAL)) { scaleSize *= 3.280839895; // feet unitLabel = "ft"; if (scaleSize > 5280) { scaleSize /= 5280; unitLabel = "mile(s)"; } } // Rounded division size int pot = (int) Math.floor(Math.log10(scaleSize)); if (!Double.isNaN(pot)) { int digit = Integer.parseInt(String.format("%.0f", scaleSize).substring(0, 1)); double divSize = digit * Math.pow(10, pot); if (digit >= 5) divSize = 5 * Math.pow(10, pot); else if (digit >= 2) divSize = 2 * Math.pow(10, pot); double divWidth = width * divSize / scaleSize; // Draw scale if (!dc.isPickingMode()) { // Set color using current layer opacity Color backColor = this.getBackgroundColor(this.color); float[] colorRGB = backColor.getRGBColorComponents(null); gl.glColor4d( colorRGB[0], colorRGB[1], colorRGB[2], (double) backColor.getAlpha() / 255d * this.getOpacity()); gl.glTranslated((width - divWidth) / 2, 0d, 0d); this.drawScale(dc, divWidth, height); colorRGB = this.color.getRGBColorComponents(null); gl.glColor4d(colorRGB[0], colorRGB[1], colorRGB[2], this.getOpacity()); gl.glTranslated(-1d / scale, 1d / scale, 0d); this.drawScale(dc, divWidth, height); // Draw label String label = String.format("%.0f ", divSize) + unitLabel; gl.glLoadIdentity(); gl.glDisable(GL.GL_CULL_FACE); drawLabel( dc, label, locationSW.add3( new Vec4(divWidth * scale / 2 + (width - divWidth) / 2, height * scale, 0))); } else { // Picking this.pickSupport.clearPickList(); this.pickSupport.beginPicking(dc); // Draw unique color across the map Color color = dc.getUniquePickColor(); int colorCode = color.getRGB(); // Add our object(s) to the pickable list this.pickSupport.addPickableObject(colorCode, this, referencePosition, false); gl.glColor3ub((byte) color.getRed(), (byte) color.getGreen(), (byte) color.getBlue()); gl.glTranslated((width - divWidth) / 2, 0d, 0d); this.drawRectangle(dc, divWidth, height); // Done picking this.pickSupport.endPicking(dc); this.pickSupport.resolvePick(dc, dc.getPickPoint(), this); } } } } finally { if (projectionPushed) { gl.glMatrixMode(GL.GL_PROJECTION); gl.glPopMatrix(); } if (modelviewPushed) { gl.glMatrixMode(GL.GL_MODELVIEW); gl.glPopMatrix(); } if (attribsPushed) gl.glPopAttrib(); } } // Draw scale rectangle private void drawRectangle(DrawContext dc, double width, double height) { GL gl = dc.getGL(); gl.glBegin(GL.GL_POLYGON); gl.glVertex3d(0, height, 0); gl.glVertex3d(0, 0, 0); gl.glVertex3d(width, 0, 0); gl.glVertex3d(width, height, 0); gl.glVertex3d(0, height, 0); gl.glEnd(); } // Draw scale graphic private void drawScale(DrawContext dc, double width, double height) { GL gl = dc.getGL(); gl.glBegin(GL.GL_LINE_STRIP); gl.glVertex3d(0, height, 0); gl.glVertex3d(0, 0, 0); gl.glVertex3d(width, 0, 0); gl.glVertex3d(width, height, 0); gl.glEnd(); gl.glBegin(GL.GL_LINE_STRIP); gl.glVertex3d(width / 2, 0, 0); gl.glVertex3d(width / 2, height / 2, 0); gl.glEnd(); } // Draw the scale label private void drawLabel(DrawContext dc, String text, Vec4 screenPoint) { TextRenderer textRenderer = OGLTextRenderer.getOrCreateTextRenderer(dc.getTextRendererCache(), this.defaultFont); Rectangle2D nameBound = textRenderer.getBounds(text); int x = (int) (screenPoint.x() - nameBound.getWidth() / 2d); int y = (int) screenPoint.y(); textRenderer.begin3DRendering(); textRenderer.setColor(this.getBackgroundColor(this.color)); textRenderer.draw(text, x + 1, y - 1); textRenderer.setColor(this.color); textRenderer.draw(text, x, y); textRenderer.end3DRendering(); } private final float[] compArray = new float[4]; // Compute background color for best contrast private Color getBackgroundColor(Color color) { Color.RGBtoHSB(color.getRed(), color.getGreen(), color.getBlue(), compArray); if (compArray[2] > 0.5) return new Color(0, 0, 0, 0.7f); else return new Color(1, 1, 1, 0.7f); } private double computeScale(java.awt.Rectangle viewport) { if (this.resizeBehavior.equals(AVKey.RESIZE_SHRINK_ONLY)) { return Math.min(1d, (this.toViewportScale) * viewport.width / this.size.width); } else if (this.resizeBehavior.equals(AVKey.RESIZE_STRETCH)) { return (this.toViewportScale) * viewport.width / this.size.width; } else if (this.resizeBehavior.equals(AVKey.RESIZE_KEEP_FIXED_SIZE)) { return 1d; } else { return 1d; } } private Vec4 computeLocation(java.awt.Rectangle viewport, double scale) { double scaledWidth = scale * this.size.width; double scaledHeight = scale * this.size.height; double x; double y; if (this.locationCenter != null) { x = this.locationCenter.x - scaledWidth / 2; y = this.locationCenter.y - scaledHeight / 2; } else if (this.position.equals(AVKey.NORTHEAST)) { x = viewport.getWidth() - scaledWidth - this.borderWidth; y = viewport.getHeight() - scaledHeight - this.borderWidth; } else if (this.position.equals(AVKey.SOUTHEAST)) { x = viewport.getWidth() - scaledWidth - this.borderWidth; y = 0d + this.borderWidth; } else if (this.position.equals(AVKey.NORTHWEST)) { x = 0d + this.borderWidth; y = viewport.getHeight() - scaledHeight - this.borderWidth; } else if (this.position.equals(AVKey.SOUTHWEST)) { x = 0d + this.borderWidth; y = 0d + this.borderWidth; } else // use North East { x = viewport.getWidth() - scaledWidth / 2 - this.borderWidth; y = viewport.getHeight() - scaledHeight / 2 - this.borderWidth; } if (this.locationOffset != null) { x += this.locationOffset.x; y += this.locationOffset.y; } return new Vec4(x, y, 0); } @Override public String toString() { return Logging.getMessage("layers.Earth.ScalebarLayer.Name"); } }