/*******************************************************************************
* Copyright 2012 Geoscience Australia
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
******************************************************************************/
package au.gov.ga.earthsci.worldwind.common.layers.fps;
import gov.nasa.worldwind.avlist.AVKey;
import gov.nasa.worldwind.geom.Vec4;
import gov.nasa.worldwind.layers.AbstractLayer;
import gov.nasa.worldwind.render.DrawContext;
import gov.nasa.worldwind.render.OrderedRenderable;
import gov.nasa.worldwind.util.Logging;
import gov.nasa.worldwind.util.OGLStackHandler;
import gov.nasa.worldwind.util.OGLTextRenderer;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.Point;
import java.awt.geom.Rectangle2D;
import javax.media.opengl.GL2;
import com.jogamp.opengl.util.awt.TextRenderer;
/**
* Layer that shows an FPS counter.
*
* @author Michael de Hoog (michael.dehoog@ga.gov.au)
*/
public class FPSLayer extends AbstractLayer
{
private Long lastNanos;
private int frameCount = 0;
private int fps;
// 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.NORTHWEST;
private String resizeBehavior = AVKey.RESIZE_SHRINK_ONLY;
private Font defaultFont = Font.decode("Arial-PLAIN-12");
private double toViewportScale = 0.2;
private Vec4 locationCenter = null;
private Vec4 locationOffset = new Vec4(-72, 5);
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
{
@Override
public double getDistanceFromEye()
{
return 0;
}
@Override
public void pick(DrawContext dc, Point pickPoint)
{
}
@Override
public void render(DrawContext dc)
{
FPSLayer.this.draw(dc);
}
}
/**
* Renders a scalebar graphic in a screen corner
*/
public FPSLayer()
{
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
* @see #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
* @see #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;
}
/**
* 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)
{
if (dc.isPickingMode())
{
return;
}
frameCount++;
long currentNanos = System.nanoTime();
if (lastNanos == null)
{
lastNanos = currentNanos;
}
else if ((currentNanos - lastNanos) / 1e9d > 1d)
{
fps = frameCount;
frameCount = 0;
lastNanos = currentNanos;
}
GL2 gl = dc.getGL().getGL2();
OGLStackHandler ogsh = new OGLStackHandler();
try
{
ogsh.pushAttrib(gl, GL2.GL_TRANSFORM_BIT);
gl.glDisable(GL2.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();
ogsh.pushProjectionIdentity(gl);
double maxwh = width > height ? width : height;
gl.glOrtho(0d, viewport.width, 0d, viewport.height, -0.6 * maxwh, 0.6 * maxwh);
ogsh.pushModelviewIdentity(gl);
// 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);
// Draw fps
gl.glEnable(GL2.GL_BLEND);
gl.glBlendFunc(GL2.GL_SRC_ALPHA, GL2.GL_ONE_MINUS_SRC_ALPHA);
float[] colorRGB = this.color.getRGBColorComponents(null);
gl.glColor4d(colorRGB[0], colorRGB[1], colorRGB[2], this.getOpacity());
// Draw label
int divWidth = 0;
String label = fps + " fps";
gl.glLoadIdentity();
gl.glDisable(GL2.GL_CULL_FACE);
drawLabel(dc, label,
locationSW.add3(new Vec4(divWidth * scale / 2 + (width - divWidth) / 2, height * scale, 0)));
}
finally
{
gl.glColor4d(1d, 1d, 1d, 1d); // restore the default OpenGL color
gl.glEnable(GL2.GL_DEPTH_TEST);
if (!dc.isPickingMode())
{
gl.glBlendFunc(GL2.GL_ONE, GL2.GL_ZERO); // restore to default blend function
gl.glDisable(GL2.GL_BLEND); // restore to default blend state
}
ogsh.pop(gl);
}
}
// 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);
}
}