Ejemplo n.º 1
0
/**
 * 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);
  }
}
Ejemplo n.º 2
0
/**
 * 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");
  }
}