package gov.nasa.worldwind.layers;
import com.sun.opengl.util.j2d.TextRenderer;
import gov.nasa.worldwind.WorldWindow;
import gov.nasa.worldwind.avlist.AVKey;
import gov.nasa.worldwind.event.*;
import gov.nasa.worldwind.geom.*;
import gov.nasa.worldwind.layers.AbstractLayer;
import gov.nasa.worldwind.pick.PickSupport;
import gov.nasa.worldwind.render.*;
import gov.nasa.worldwind.util.Logging;
import gov.nasa.worldwind.view.*;
import javax.media.opengl.GL;
import java.awt.*;
import java.awt.geom.*;
import java.beans.PropertyChangeEvent;
import java.util.*;
/**
* Displays a terrain profile graph in a screen corner.
* <p>
* Usage: do setEventSource(wwd) to have the graph activated and updated with position changes See public
* properties for options: keepProportions, follow, unit, start and end latlon...
* </p>
* @author Patrick Murris
* @version $Id: TerrainProfileLayer.java 5176 2008-04-25 21:31:06Z patrickmurris $
*/
public class TerrainProfileLayer extends AbstractLayer implements PositionListener, SelectListener
{
// Positionning constants TODO: add north, east... center-north... center-screen.
public final static String NORTHWEST = "gov.nasa.worldwind.TerrainProfileLayer.NorthWest";
public final static String SOUTHWEST = "gov.nasa.worldwind.TerrainProfileLayer.SouthWest";
public final static String NORTHEAST = "gov.nasa.worldwind.TerrainProfileLayer.NorthEast";
public final static String SOUTHEAST = "gov.nasa.worldwind.TerrainProfileLayer.SouthEast";
// Stretching behavior constants
public final static String RESIZE_STRETCH = "gov.nasa.worldwind.TerrainProfileLayer.Stretch";
public final static String RESIZE_SHRINK_ONLY = "gov.nasa.worldwind.TerrainProfileLayer.ShrinkOnly";
public final static String RESIZE_KEEP_FIXED_SIZE = "gov.nasa.worldwind.TerrainProfileLayer.FixedSize";
// Units constants
public final static String UNIT_METRIC = "gov.nasa.worldwind.TerrainProfileLayer.Metric";
public final static String UNIT_IMPERIAL = "gov.nasa.worldwind.TerrainProfileLayer.Imperial";
public final static double METER_TO_FEET = 3.280839895;
// Follow constants
public final static String FOLLOW_VIEW = "gov.nasa.worldwind.TerrainProfileLayer.FollowView";
public final static String FOLLOW_EYE = "gov.nasa.worldwind.TerrainProfileLayer.FollowEye";
public final static String FOLLOW_CURSOR = "gov.nasa.worldwind.TerrainProfileLayer.FollowCursor";
public final static String FOLLOW_NONE = "gov.nasa.worldwind.TerrainProfileLayer.FollowNone";
public final static String FOLLOW_OBJECT = "gov.nasa.worldwind.TerrainProfileLayer.FollowObject";
// Graph size when minimized
private final static int MINIMIZED_SIZE = 32;
// GUI pickable objects
private final String buttonMinimize = new String("gov.nasa.worldwind.TerrainProfileLayer.ButtonMinimize");
private final String buttonMaximize = new String("gov.nasa.worldwind.TerrainProfileLayer.ButtonMaximize");
// Display parameters - TODO: make configurable
private Dimension size = new Dimension(250, 100);
private Color color = Color.white;
private int borderWidth = 20;
private String position = SOUTHWEST;
private String resizeBehavior = RESIZE_SHRINK_ONLY;
private String unit = UNIT_METRIC;
private Font defaultFont = Font.decode("Arial-PLAIN-12");
private double toViewportScale = 1;
private Point locationCenter = null;
private TextRenderer textRenderer = null;
private PickSupport pickSupport = new PickSupport();
private boolean initialized = false;
private boolean isMinimized = false; // True when graph is minimized to an icon
private boolean isMaximized = false; // True when graph is 'full screen'
private boolean update = true; // Recompute profile if true
private int pickedSample = -1; // Picked sample number if not -1
Polyline selectionShape; // Shape showing section on the ground
Polyline pickedShape; // Shape showing actual pick position on the ground
private boolean keepProportions = false; // Keep graph distance/elevation proportions
private boolean zeroBased = true; // Pad graph elevation scale to include sea level if true
private String follow = FOLLOW_VIEW; // Profile position follow behavior
private boolean showEyePosition = false; // When FOLLOW_EYE, draw the eye position on graph when true
private double profileLengthFactor = 1; // Applied to default profile length (zoom on profile)
private LatLon startLatLon; // Section start lat/lon when FOLLOW_NONE
private LatLon endLatLon; // Section end lat/lon when FOLLOW_NONE
private Position objectPosition; // Object position if FOLLOW_OBJECT
private Angle objectHeading; // Object heading if FOLLOW_OBJECT
// Terrain profile data
private int samples = 250; // Number of position samples
private double minElevation; // Minimum elevation along the profile
private double maxElevation; // Maximum elevation along the profile
private double length; // Profile length along great circle in meter
private Position positions[]; // Position list
// Worldwind
private WorldWindow wwd;
// 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)
{
TerrainProfileLayer.this.drawProfile(dc);
}
public void render(DrawContext dc)
{
TerrainProfileLayer.this.drawProfile(dc);
}
}
/**
* Renders a terrain profile graphic in a screen corner
*/
public TerrainProfileLayer()
{
this.setName(Logging.getMessage("layers.Earth.TerrainProfileLayer.Name"));
}
// ** Public properties ************************************************************
/**
* Get whether the profile should be recomputed
*
* @return true if the profile should be recomputed
*/
public boolean getUpdate()
{
return this.update;
}
/**
* Set wheter the profile should be recomputed
*
* @param state true if the profile should be recomputed
*/
public void setUpdate(boolean state)
{
this.update = state;
}
/**
* Get whether the profile graph is minimized
*
* @return true if the profile graph is minimized
*/
public boolean getIsMinimized()
{
return this.isMinimized;
}
/**
* Set wheter the profile graph should be minimized.
* <p>Note that the graph can be both minimized and maximized at the same time.
* The minimized state will take precedence and the graph will display as an icon.
* When 'un-minimized' it will display as maximized.</p>
*
* @param state true if the profile should be minimized
* @see this.setIsMaximized()
*/
public void setIsMinimized(boolean state)
{
this.isMinimized = state;
this.pickedSample = -1; // Clear picked position
}
/**
* Get whether the profile graph is maximized - displays over the whole viewport.
*
* @return true if the profile graph is maximized
*/
public boolean getIsMaximized()
{
return this.isMaximized;
}
/**
* Set wheter the profile graph should be maximized - displays over the whole viewport.
*
* @param state true if the profile should be maximized
*/
public void setIsMaximized(boolean state)
{
this.isMaximized = state;
}
/**
* Get the graphic Dimension (in pixels)
*
* @return the scalebar graphic Dimension
*/
public Dimension getSize()
{
return this.size;
}
/**
* Set the graphic Dimenion (in pixels)
*
* @param size the 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 graphic color
*
* @return the graphic Color
*/
public Color getColor()
{
return this.color;
}
/**
* Set the graphic Color
*
* @param color the graphic 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 graphic-to-viewport scale factor.
*
* @return the graphic-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 graphic. This scale factor
* is used only when the layer's resize behavior is {@link #RESIZE_STRETCH} or {@link #RESIZE_SHRINK_ONLY}. The
* graphic's width is adjusted to occupy the proportion of the viewport's width indicated by this factor. The graphic's
* height is adjusted to maintain the graphic's Dimension aspect ratio.
*
* @param toViewportScale the graphic 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 graphic. Can be one of {@link #NORTHEAST} (the default), {@link
* #NORTHWEST}, {@link #SOUTHEAST}, or {@link #SOUTHWEST}. These indicate the corner of the viewport.
*
* @param position the desired graphic 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;
}
/**
* Get the screen location of the graph center if set (can be null)
*
* @return the screen location of the graph center if set (can be null)
*/
public Point getLocationCenter()
{
return this.locationCenter;
}
/**
* Set the screen location of the graph center - overrides SetPosition if not null
*
* @param point the screen location of the graph center (can be null)
*/
public void setLocationCenter(Point point)
{
this.locationCenter = point;
}
/**
* 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 graphic when the viewport size changes, typically when the World Wind
* window is resized. If the value is {@link #RESIZE_KEEP_FIXED_SIZE}, the graphic size is kept to the size specified
* in its Dimension scaled by the layer's current icon scale. If the value is {@link #RESIZE_STRETCH}, the graphic is
* resized to have a constant size relative to the current viewport size. If the viewport shrinks the graphic size
* decreases; if it expands then the graphic enlarges. If the value is {@link #RESIZE_SHRINK_ONLY} (the default),
* graphic sizing behaves as for {@link #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 graphic offset from the viewport border.
*
* @param borderWidth the number of pixels to offset the graphic 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 graphic uses to display distances and elevations. 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 graphic legend Font
*
* @return the graphic legend Font
*/
public Font getFont()
{
return this.defaultFont;
}
/**
* Set the graphic legend Font
*
* @param font the graphic 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;
}
/**
* Get whether distance/elevation proportions are maintained
*
* @return true if the graph maintains distance/elevation proportions
*/
public boolean getKeepProportions()
{
return this.keepProportions;
}
/**
* Set whether distance/elevation proportions are maintained
*
* @param state true if the graph should maintains distance/elevation proportions
*/
public void setKeepProportions(boolean state)
{
this.keepProportions = state;
}
/**
* Get whether the graph center point follows the mouse cursor
*
* @return true if the graph center point follows the mouse cursor
*/
public boolean getFollowCursor()
{
return this.follow.equals(FOLLOW_CURSOR);
}
/**
* Set whether the graph center point should follows the mouse cursor
*
* @param state true if the graph center point should follows the mouse cursor
*/
public void setFollowCursor(boolean state)
{
this.setFollow(state ? FOLLOW_CURSOR : FOLLOW_VIEW);
}
/**
* Get the graph center point placement behavior
*
* @return the graph center point placement behavior
*/
public String getFollow()
{
return this.follow;
}
/**
* Set the graph center point placement behavior. Can be one of {@link #FOLLOW_VIEW} (the default),
* {@link #FOLLOW_CURSOR}, {@link #FOLLOW_EYE} or {@link #FOLLOW_NONE}. If {@link #FOLLOW_NONE} the profile will
* be computed between startLatLon and endLatLon.
*
* @param behavior the graph center point placement behavior
*/
public void setFollow(String behavior)
{
this.follow = behavior;
this.setUpdate(true);
}
/**
* Get whether the eye position is shown on the graph when {@link #FOLLOW_EYE}
*
* @return true if the eye position is shown on the grap
*/
public boolean getShowEyePosition()
{
return this.showEyePosition;
}
/**
* Set whether the eye position is shown on the graph when {@link #FOLLOW_EYE}
*
* @param state if true the eye position is shown on the graph
*/
public void setShowEyePosition(Boolean state)
{
this.showEyePosition = state;
if (this.follow.equals(FOLLOW_EYE))
this.setUpdate(true);
}
/**
* Set the profile length factor - has no effect if {@link #FOLLOW_NONE}
*
* @param factor the new factor
*/
public void setProfileLengthFactor(double factor)
{
this.profileLengthFactor = factor;
this.setUpdate(true);
}
/**
* Get the profile length factor
*
* @return the profile length factor
*/
public double getProfileLenghtFactor()
{
return this.profileLengthFactor;
}
/**
* Get the profile start position lat/lon when {@link #FOLLOW_NONE}
*
* @return the profile start position lat/lon
*/
public LatLon getStartLatLon()
{
return this.startLatLon;
}
/**
* Set the profile start position lat/lon when {@link #FOLLOW_NONE}
*
* @param latLon the profile start position lat/lon
*/
public void setStartLatLon(LatLon latLon)
{
if (latLon == null)
{
String msg = Logging.getMessage("nullValue.LatLonIsNull");
Logging.logger().severe(msg);
throw new IllegalArgumentException(msg);
}
this.startLatLon = latLon;
if (this.follow.equals(FOLLOW_NONE))
this.setUpdate(true);
}
/**
* Get the profile end position lat/lon when {@link #FOLLOW_NONE}
*
* @return the profile end position lat/lon
*/
public LatLon getEndLatLon()
{
return this.endLatLon;
}
/**
* Set the profile end position lat/lon when {@link #FOLLOW_NONE}
*
* @param latLon the profile end position lat/lon
*/
public void setEndLatLon(LatLon latLon)
{
if (latLon == null)
{
String msg = Logging.getMessage("nullValue.LatLonIsNull");
Logging.logger().severe(msg);
throw new IllegalArgumentException(msg);
}
this.endLatLon = latLon;
if (this.follow.equals(FOLLOW_NONE))
this.setUpdate(true);
}
/**
* Get the number of elevation samples in the profile
*
* @return the number of elevation samples in the profile
*/
public int getSamples()
{
return this.samples;
}
/**
* Set the number of elevation samples in the profile
*
* @param number the number of elevation samples in the profile
*/
public void setSamples(int number)
{
this.samples = Math.abs(number);
this.setUpdate(true);
}
/**
* Get whether the profile graph should include sea level
*
* @return whether the profile graph should include sea level
*/
public boolean getZeroBased()
{
return this.zeroBased;
}
/**
* Set whether the profile graph should include sea level. True is the default.
*
* @param state true if the profile graph should include sea level
*/
public void setZeroBased(boolean state)
{
this.zeroBased = state;
this.setUpdate(true);
}
public Position getObjectPosition()
{
return this.objectPosition;
}
public void setObjectPosition(Position pos)
{
this.objectPosition = pos;
if (this.follow.equals(FOLLOW_OBJECT))
this.setUpdate(true);
}
public Angle getObjectHeading()
{
return this.objectHeading;
}
public void setObjectHeading(Angle heading)
{
this.objectHeading = heading;
if (this.follow.equals(FOLLOW_OBJECT))
this.setUpdate(true);
}
// ** Rendering ************************************************************
@Override
public void doRender(DrawContext dc)
{
// Delegate graph rendering to OrderedRenderable list
dc.addOrderedRenderable(this.orderedImage);
// Render section line on the ground now
if (!isMinimized && this.positions != null && this.selectionShape != null)
{
this.selectionShape.render(dc);
// If picking in progress, render pick indicator
if (this.pickedSample != -1 && this.pickedShape != null)
this.pickedShape.render(dc);
}
}
@Override
public void doPick(DrawContext dc, Point pickPoint)
{
dc.addOrderedRenderable(this.orderedImage);
}
private void initialize(DrawContext dc)
{
if (this.initialized || this.positions != null)
return;
if (this.wwd != null)
this.computeProfile(dc);
if (this.positions != null)
this.initialized = true;
}
// Profile graph rendering - ortho
public void drawProfile(DrawContext dc)
{
if (this.update)
this.computeProfile(dc);
if ((this.positions == null || (this.minElevation == 0 && this.maxElevation == 0))
&& !this.initialized)
this.initialize(dc);
if (this.positions == null || (this.minElevation == 0 && this.maxElevation == 0))
return;
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);
Rectangle viewport = dc.getView().getViewport();
Dimension drawSize = isMinimized ? new Dimension(MINIMIZED_SIZE, MINIMIZED_SIZE) :
isMaximized ? new Dimension(viewport.width - this.borderWidth * 2,
viewport.height * 2 / 3 - this.borderWidth * 2) : this.size;
double width = drawSize.width;
double height = drawSize.height;
// Load a parallel projection with xy dimensions (viewportWidth, viewportHeight)
// into the GL projection matrix.
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, 1d);
if (!dc.isPickingMode())
{
// Draw grid - Set color using current layer opacity
this.drawGrid(dc, drawSize);
// Draw profile graph
this.drawGraph(dc, drawSize);
if (!isMinimized)
{
// Draw GUI buttons
drawGUI(dc, drawSize);
// Draw labels
String label = String.format("min %.0fm max %.0fm", this.minElevation, this.maxElevation);
if (this.unit.equals(UNIT_IMPERIAL))
label = String.format("min %.0fft max %.0fft", this.minElevation * METER_TO_FEET,
this.maxElevation * METER_TO_FEET);
gl.glLoadIdentity();
gl.glDisable(GL.GL_CULL_FACE);
drawLabel(label, locationSW.add3(new Vec4(0, -12, 0)), -1); // left aligned
if (this.pickedSample != -1)
{
double pickedElevation = positions[this.pickedSample].getElevation();
label = String.format("%.0fm", pickedElevation);
if (this.unit.equals(UNIT_IMPERIAL))
label = String.format("%.0fft", pickedElevation * METER_TO_FEET);
drawLabel(label, locationSW.add3(new Vec4(width, -12, 0)), 1); // right aligned
}
}
}
else
{
// Picking
this.pickSupport.clearPickList();
this.pickSupport.beginPicking(dc);
// Draw unique color across the rectangle
Color color = dc.getUniquePickColor();
int colorCode = color.getRGB();
if (!isMinimized)
{
// Update graph pick point
computePickPosition(dc, locationSW, new Dimension((int) (width * scale), (int) (height * scale)));
// Draw GUI buttons
drawGUI(dc, drawSize);
}
else
{
// Add graph to the pickable list for 'un-minimize' click
this.pickSupport.addPickableObject(colorCode, this);
gl.glColor3ub((byte) color.getRed(), (byte) color.getGreen(), (byte) color.getBlue());
gl.glBegin(GL.GL_POLYGON);
gl.glVertex3d(0, 0, 0);
gl.glVertex3d(width, 0, 0);
gl.glVertex3d(width, height, 0);
gl.glVertex3d(0, height, 0);
gl.glVertex3d(0, 0, 0);
gl.glEnd();
}
// Done picking
this.pickSupport.endPicking(dc);
this.pickSupport.resolvePick(dc, dc.getPickPoint(), this);
}
}
catch (Exception e)
{
e.printStackTrace();
}
finally
{
if (projectionPushed)
{
gl.glMatrixMode(GL.GL_PROJECTION);
gl.glPopMatrix();
}
if (modelviewPushed)
{
gl.glMatrixMode(GL.GL_MODELVIEW);
gl.glPopMatrix();
}
if (attribsPushed)
gl.glPopAttrib();
}
}
// Draw grid graphic
private void drawGrid(DrawContext dc, Dimension dimension)
{
// Background color
Color backColor = getBackgroundColor(this.color);
drawFilledRectangle(dc, new Vec4(0, 0, 0), dimension, new Color(backColor.getRed(),
backColor.getGreen(), backColor.getBlue(), (int) (backColor.getAlpha() * .5))); // Increased transparency
// Grid - minimal
float[] colorRGB = this.color.getRGBColorComponents(null);
dc.getGL().glColor4d(colorRGB[0], colorRGB[1], colorRGB[2], this.getOpacity());
drawVerticalLine(dc, dimension, 0);
drawVerticalLine(dc, dimension, dimension.getWidth());
drawHorizontalLine(dc, dimension, 0);
}
// Draw profile graphic
private void drawGraph(DrawContext dc, Dimension dimension)
{
GL gl = dc.getGL();
// Adjust min/max elevation for the graph
double min = this.minElevation;
double max = this.maxElevation;
if (this.showEyePosition && this.follow.equals(FOLLOW_EYE))
max = Math.max(max, dc.getView().getEyePosition().getElevation());
if (this.showEyePosition && this.follow.equals(FOLLOW_OBJECT) && this.objectPosition != null)
max = Math.max(max, this.objectPosition.getElevation());
if (this.zeroBased)
{
if (min > 0) min = 0;
if (max < 0) max = 0;
}
int i;
double stepX = dimension.getWidth() / (this.length);
double stepY = dimension.getHeight() / (max - min);
if (this.keepProportions)
{
stepX = Math.min(stepX, stepY);
stepY = stepX;
}
double lengthStep = this.length / (this.samples - 1);
double x = 0, y = 0;
// Filled graph
gl.glColor4ub((byte) this.color.getRed(), (byte) this.color.getGreen(),
(byte) this.color.getBlue(), (byte) 100);
gl.glBegin(GL.GL_TRIANGLE_STRIP);
for (i = 0; i < this.samples; i++)
{
x = i * lengthStep * stepX;
y = (this.positions[i].getElevation() - min) * stepY;
gl.glVertex3d(x, 0, 0);
gl.glVertex3d(x, y, 0);
}
gl.glEnd();
// Line graph
float[] colorRGB = this.color.getRGBColorComponents(null);
gl.glColor4d(colorRGB[0], colorRGB[1], colorRGB[2], this.getOpacity());
gl.glBegin(GL.GL_LINE_STRIP);
for (i = 0; i < this.samples; i++)
{
x = i * lengthStep * stepX;
y = (this.positions[i].getElevation() - min) * stepY;
gl.glVertex3d(x, y, 0);
}
gl.glEnd();
// Middle vertical line
gl.glColor4d(colorRGB[0], colorRGB[1], colorRGB[2], this.getOpacity() * .3); // increased transparency here
drawVerticalLine(dc, dimension, x / 2);
// Eye position
if ((this.follow.equals(FOLLOW_EYE) ||
(this.follow.equals(FOLLOW_OBJECT) && this.objectPosition != null))
&& this.showEyePosition)
{
double eyeY = (dc.getView().getEyePosition().getElevation() - min) * stepY;
if (this.follow.equals(FOLLOW_OBJECT))
eyeY = (this.objectPosition.getElevation() - min) * stepY;
this.drawFilledRectangle(dc, new Vec4(x / 2 - 2, eyeY - 2, 0), new Dimension(5, 5), this.color);
}
// Selected/picked vertical and horizontal lines
if (this.pickedSample != -1)
{
double pickedX = this.pickedSample * lengthStep * stepX;
double pickedY = (positions[this.pickedSample].getElevation() - min) * stepY;
gl.glColor4d(colorRGB[0], colorRGB[1], colorRGB[2] * .5, this.getOpacity() * .8); // yellower color
drawVerticalLine(dc, dimension, pickedX);
drawHorizontalLine(dc, dimension, pickedY);
// Eye or object - picked position line
if ((this.follow.equals(FOLLOW_EYE) ||
(this.follow.equals(FOLLOW_OBJECT) && this.objectPosition != null))
&& this.showEyePosition)
{
// Line
double eyeY = (dc.getView().getEyePosition().getElevation() - min) * stepY;
if (this.follow.equals(FOLLOW_OBJECT))
eyeY = (this.objectPosition.getElevation() - min) * stepY;
drawLine(dc, pickedX, pickedY, x / 2, eyeY);
// Distance label
double distance = dc.getView().getEyePoint().distanceTo3(
dc.getGlobe().computePointFromPosition(positions[this.pickedSample]));
if (this.follow.equals(FOLLOW_OBJECT))
distance = dc.getGlobe().computePointFromPosition(this.objectPosition).distanceTo3(
dc.getGlobe().computePointFromPosition(positions[this.pickedSample]));
String label = String.format("Dist %.0fm", distance);
if (this.unit.equals(UNIT_IMPERIAL))
label = String.format("Dist %.0fft", distance * METER_TO_FEET);
drawLabel(label, new Vec4(pickedX + 5, pickedY - 12, 0), -1); // left aligned
}
}
// Min elevation horizontal line
if (this.minElevation != min)
{
y = (this.minElevation - min) * stepY;
gl.glColor4d(colorRGB[0], colorRGB[1], colorRGB[2], this.getOpacity() * .5); // medium transparency
drawHorizontalLine(dc, dimension, y);
}
// Max elevation horizontal line
if (this.maxElevation != max)
{
y = (this.maxElevation - min) * stepY;
gl.glColor4d(colorRGB[0], colorRGB[1], colorRGB[2], this.getOpacity() * .5); // medium transparency
drawHorizontalLine(dc, dimension, y);
}
// Sea level in between positive elevations only (not across land)
if (min < 0 && max >= 0)
{
gl.glColor4d(colorRGB[0] * .7, colorRGB[1] * .7, colorRGB[2], this.getOpacity() * .5); // bluer color
y = -this.minElevation * stepY;
double previousX = -1;
for (i = 0; i < this.samples; i++)
{
x = i * lengthStep * stepX;
if (this.positions[i].getElevation() > 0 || i == this.samples - 1)
{
if (previousX >= 0)
{
gl.glBegin(GL.GL_LINE_STRIP);
gl.glVertex3d(previousX, y, 0);
gl.glVertex3d(x, y, 0);
gl.glEnd();
previousX = -1;
}
}
else
previousX = previousX < 0 ? x : previousX;
}
}
}
private void drawGUI(DrawContext dc, Dimension dimension)
{
GL gl = dc.getGL();
int buttonSize = 16;
int hs = buttonSize / 2;
int buttonBorder = 4;
Dimension buttonDimension = new Dimension(buttonSize, buttonSize);
Color highlightColor = new Color(color.getRed(), color.getGreen(),
color.getBlue(), (int) (color.getAlpha() * .5));
Color backColor = getBackgroundColor(this.color);
backColor = new Color(backColor.getRed(), backColor.getGreen(),
backColor.getBlue(), (int) (backColor.getAlpha() * .5)); // Increased transparency
Color drawColor;
int y = dimension.height - buttonDimension.height - buttonBorder;
int x = dimension.width;
Object pickedObject = dc.getPickedObjects() != null ? dc.getPickedObjects().getTopObject() : null;
// Maximize button
if (!isMaximized)
{
x -= buttonDimension.width + buttonBorder;
if (dc.isPickingMode())
{
drawColor = dc.getUniquePickColor();
int colorCode = drawColor.getRGB();
this.pickSupport.addPickableObject(colorCode, this.buttonMaximize, null, false);
}
else
drawColor = this.buttonMaximize == pickedObject ? highlightColor : backColor;
drawFilledRectangle(dc, new Vec4(x, y, 0), buttonDimension, drawColor);
if (!dc.isPickingMode())
{
gl.glColor4ub((byte) color.getRed(), (byte) color.getGreen(),
(byte) color.getBlue(), (byte) color.getAlpha());
// Draw '+'
drawLine(dc, x + 3, y + hs, x + buttonDimension.width - 3, y + hs); // Horizontal line
drawLine(dc, x + hs, y + 3, x + hs, y + buttonDimension.height - 3); // Vertical line
}
}
// Minimize button
x -= buttonDimension.width + buttonBorder;
if (dc.isPickingMode())
{
drawColor = dc.getUniquePickColor();
int colorCode = drawColor.getRGB();
this.pickSupport.addPickableObject(colorCode, this.buttonMinimize, null, false);
}
else
drawColor = this.buttonMinimize == pickedObject ? highlightColor : backColor;
drawFilledRectangle(dc, new Vec4(x, y, 0), buttonDimension, drawColor);
if (!dc.isPickingMode())
{
gl.glColor4ub((byte) color.getRed(), (byte) color.getGreen(),
(byte) color.getBlue(), (byte) color.getAlpha());
// Draw '-'
drawLine(dc, x + 3, y + hs, x + buttonDimension.width - 3, y + hs); // Horizontal line
}
}
private void drawHorizontalLine(DrawContext dc, Dimension dimension, double y)
{
drawLine(dc, 0, y, dimension.getWidth(), y);
}
private void drawVerticalLine(DrawContext dc, Dimension dimension, double x)
{
drawLine(dc, x, 0, x, dimension.getHeight());
}
private void drawFilledRectangle(DrawContext dc, Vec4 origin, Dimension dimension, Color color)
{
GL gl = dc.getGL();
gl.glColor4ub((byte) color.getRed(), (byte) color.getGreen(),
(byte) color.getBlue(), (byte) color.getAlpha());
gl.glDisable(GL.GL_TEXTURE_2D); // no textures
gl.glBegin(GL.GL_POLYGON);
gl.glVertex3d(origin.x, origin.y, 0);
gl.glVertex3d(origin.x + dimension.getWidth(), origin.y, 0);
gl.glVertex3d(origin.x + dimension.getWidth(), origin.y + dimension.getHeight(), 0);
gl.glVertex3d(origin.x, origin.y + dimension.getHeight(), 0);
gl.glVertex3d(origin.x, origin.y, 0);
gl.glEnd();
}
private void drawLine(DrawContext dc, double x1, double y1, double x2, double y2)
{
GL gl = dc.getGL();
gl.glBegin(GL.GL_LINE_STRIP);
gl.glVertex3d(x1, y1, 0);
gl.glVertex3d(x2, y2, 0);
gl.glEnd();
}
// Draw a text label
// Align = -1: left, 0: center and 1: right
private void drawLabel(String text, Vec4 screenPoint, int align)
{
if (this.textRenderer == null)
{
this.textRenderer = new TextRenderer(this.defaultFont, true, true);
}
Rectangle2D nameBound = this.textRenderer.getBounds(text);
int x = (int) screenPoint.x(); // left
if (align == 0) x = (int) (screenPoint.x() - nameBound.getWidth() / 2d); // centered
if (align > 0) x = (int) (screenPoint.x() - nameBound.getWidth()); // right
int y = (int) screenPoint.y();
this.textRenderer.begin3DRendering();
this.textRenderer.setColor(this.getBackgroundColor(this.color));
this.textRenderer.draw(text, x + 1, y - 1);
this.textRenderer.setColor(this.color);
this.textRenderer.draw(text, x, y);
this.textRenderer.end3DRendering();
}
// Compute background color for best contrast
private Color getBackgroundColor(Color color)
{
float[] compArray = new float[4];
Color.RGBtoHSB(color.getRed(), color.getGreen(), color.getBlue(), compArray);
if (compArray[2] > 0.5)
return new Color(0, 0, 0, (int) (this.color.getAlpha() * 0.7f));
else
return new Color(255, 255, 255, (int) (this.color.getAlpha() * 0.7f));
}
// ** Dimensions and positionning ************************************************************
private double computeScale(java.awt.Rectangle viewport)
{
if (this.resizeBehavior.equals(RESIZE_SHRINK_ONLY))
{
return Math.min(1d, (this.toViewportScale) * viewport.width / this.size.width);
}
else if (this.resizeBehavior.equals(RESIZE_STRETCH))
{
return (this.toViewportScale) * viewport.width / this.size.width;
}
else if (this.resizeBehavior.equals(RESIZE_KEEP_FIXED_SIZE))
{
return 1d;
}
else
{
return 1d;
}
}
private Vec4 computeLocation(java.awt.Rectangle viewport, double scale)
{
double scaledWidth = scale * (isMinimized ? MINIMIZED_SIZE :
isMaximized ? viewport.width - this.borderWidth * 2 : this.size.width);
double scaledHeight = scale * (isMinimized ? MINIMIZED_SIZE :
isMaximized ? viewport.height * 2 / 3 - this.borderWidth * 2 : 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(NORTHEAST))
{
x = viewport.getWidth() - scaledWidth - this.borderWidth;
y = viewport.getHeight() - scaledHeight - this.borderWidth;
}
else if (this.position.equals(SOUTHEAST))
{
x = viewport.getWidth() - scaledWidth - this.borderWidth;
y = 0d + this.borderWidth;
}
else if (this.position.equals(NORTHWEST))
{
x = 0d + this.borderWidth;
y = viewport.getHeight() - scaledHeight - this.borderWidth;
}
else if (this.position.equals(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;
}
return new Vec4(x, y, 0);
}
/**
* Computes the Position of the pickPoint over the graph and updates pickedSample indice
*
* @param dc the current DrawContext
* @param locationSW the screen location of the bottom left corner of the graph
* @param mapSize the graph screen dimension in pixels
* @return the picked Position
*/
private Position computePickPosition(DrawContext dc, Vec4 locationSW, Dimension mapSize)
{
Position pickPosition = null;
this.pickedSample = -1;
Point pickPoint = dc.getPickPoint();
if (pickPoint != null && this.positions != null && !this.follow.equals(FOLLOW_CURSOR))
{
Rectangle viewport = dc.getView().getViewport();
// Check if pickpoint is inside the graph
if (pickPoint.getX() >= locationSW.getX()
&& pickPoint.getX() < locationSW.getX() + mapSize.width
&& viewport.height - pickPoint.getY() >= locationSW.getY()
&& viewport.height - pickPoint.getY() < locationSW.getY() + mapSize.height)
{
// Find sample - Note: only works when graph expends over the full width
int sample = (int) (((double) (pickPoint.getX() - locationSW.getX()) / mapSize.width) * this.samples);
if (sample >= 0 && sample < this.samples)
{
pickPosition = this.positions[sample];
this.pickedSample = sample;
// Update polyline indicator
ArrayList<Position> posList = new ArrayList<Position>();
posList.add(positions[sample]);
posList.add(new Position(positions[sample].getLatitude(), positions[sample].getLongitude(),
positions[sample].getElevation() + this.length / 10));
if (this.pickedShape == null)
{
this.pickedShape = new Polyline(posList);
this.pickedShape.setPathType(Polyline.LINEAR);
this.pickedShape.setLineWidth(2);
this.pickedShape.setColor(new Color(this.color.getRed(),
this.color.getGreen(), (int) (this.color.getBlue() * .8), (int) (255 * .8)));
}
else
this.pickedShape.setPositions(posList);
}
}
}
return pickPosition;
}
// ** Position listener impl. ************************************************************
public void moved(PositionEvent event)
{
if (this.wwd != null && this.isEnabled())
{
// Update profile if FOLLOW_CURSOR
if (this.follow.equals(FOLLOW_CURSOR))
this.setUpdate(true);
}
else
this.positions = null;
}
// ** Select listener impl. ************************************************************
public void selected(SelectEvent event)
{
if (event.hasObjects() && event.getEventAction().equals(SelectEvent.LEFT_CLICK))
{
Object o = event.getTopObject();
if (o == this.buttonMinimize)
{
if (this.isMaximized)
this.setIsMaximized(false);
else
this.setIsMinimized(true);
}
else if (o == this.buttonMaximize)
{
this.setIsMaximized(true);
}
else if (o == this && this.isMinimized)
{
this.setIsMinimized(false);
this.setUpdate(true);
}
}
}
// ** Property change listener ***********************************************************
public void propertyChange(PropertyChangeEvent propertyChangeEvent)
{
if (this.wwd != null && this.isEnabled())
{
// Update profile if FOLLOW_VIEW or FOLLOW_EYE or terrain elevations changed
if (this.follow.equals(FOLLOW_VIEW)
|| this.follow.equals(FOLLOW_EYE)
|| propertyChangeEvent.getPropertyName().equals(AVKey.ELEVATION_MODEL))
this.setUpdate(true);
}
else
this.positions = null;
}
// Sets the wwd local reference and add us to the position listeners
// the view and elevation model property change listener
public void setEventSource(WorldWindow wwd)
{
if (this.wwd != null)
{
this.wwd.removePositionListener(this);
this.wwd.getView().removePropertyChangeListener(this);
this.wwd.getModel().getGlobe().getElevationModel().removePropertyChangeListener(this);
this.wwd.removeSelectListener(this);
}
this.wwd = wwd;
if (this.wwd != null)
{
this.wwd.addPositionListener(this);
this.wwd.getView().addPropertyChangeListener(this);
this.wwd.getModel().getGlobe().getElevationModel().addPropertyChangeListener(this);
this.wwd.addSelectListener(this);
}
}
// ** Profile data collection ************************************************************
/**
* Compute the terrain profile.
* <p>
* If {@link #FOLLOW_VIEW ], {@link #FOLLOW_EYE] or {@link #FOLLOW_CURSOR] collects terrain profile data along a
* great circle line centered at the current position (view, eye or cursor) and perpendicular to the view heading.
* If {@link #FOLLOW_NONE] the profile is computed in between start and end latlon
* </p>
* @param dc the current <code>DrawContext</code>.
*/
public void computeProfile(DrawContext dc)
{
if (this.wwd == null)
return;
try
{
// Find center position
OrbitView view = (OrbitView) this.wwd.getView(); // TODO: check for OrbitView instance first
Position groundPos = view.computePositionFromScreenPoint(
view.getViewport().getWidth() / 2, view.getViewport().getHeight() / 2);
if (view.getCenterPosition() != null)
groundPos = new Position(view.getCenterPosition().getLatLon(), 0);
if (this.follow.equals(FOLLOW_CURSOR))
groundPos = computeCursorPosition(dc);
if (this.follow.equals(FOLLOW_EYE))
groundPos = view.getEyePosition();
if (this.follow.equals(FOLLOW_OBJECT))
groundPos = this.objectPosition;
if ((this.follow.equals(FOLLOW_VIEW) && groundPos != null ) ||
(this.follow.equals(FOLLOW_EYE) && groundPos != null ) ||
(this.follow.equals(FOLLOW_CURSOR) && groundPos != null ) ||
(this.follow.equals(FOLLOW_NONE) && this.startLatLon != null && this.endLatLon != null) ||
(this.follow.equals(FOLLOW_OBJECT) && this.objectPosition != null && this.objectHeading != null))
{
this.positions = new Position[samples];
this.minElevation = Double.MAX_VALUE;
this.maxElevation = -1e6; // Note: Double.MIN_VALUE would fail below min-max tests for some reason
this.length = Math.min(view.getZoom() * .8 * this.profileLengthFactor,
this.wwd.getModel().getGlobe().getRadius() * Math.PI);
if (length <= 0)
this.length = Math.min(view.getEyePosition().getElevation() * .8 * this.profileLengthFactor,
this.wwd.getModel().getGlobe().getRadius() * Math.PI);
if (this.follow.equals(FOLLOW_NONE))
this.length = LatLon.greatCircleDistance(this.startLatLon, this.endLatLon).radians
* this.wwd.getModel().getGlobe().getRadius();
if (this.follow.equals(FOLLOW_OBJECT))
this.length = Math.min(this.objectPosition.getElevation() * .8 * this.profileLengthFactor,
this.wwd.getModel().getGlobe().getRadius() * Math.PI);
double lengthRadian = this.length / this.wwd.getModel().getGlobe().getRadius();
LatLon centerLatLon = new LatLon(groundPos.getLatitude(), groundPos.getLongitude());
// Iterate on both sides of the center point
int i;
double step = lengthRadian / (samples - 1);
for (i = 0; i < this.samples; i++)
{
LatLon latLon = null;
if (!this.follow.equals(FOLLOW_NONE))
{
// Compute segments perpendicular to view or object heading
double azimuth = view.getHeading().subtract(Angle.POS90).radians;
if (this.follow.equals(FOLLOW_OBJECT))
azimuth = this.objectHeading.subtract(Angle.POS90).radians;
if (i > (float) (this.samples - 1) / 2f)
{
//azimuth = view.getHeading().subtract(Angle.NEG90).radians;
azimuth += Math.PI;
}
double distance = Math.abs(((double) i - ((double) (this.samples - 1) / 2d)) * step);
latLon = LatLon.greatCircleEndPosition(centerLatLon, azimuth, distance);
}
else if (this.follow.equals(FOLLOW_NONE) && this.startLatLon != null
&& this.endLatLon != null)
{
// Compute segments between start and end positions latlon
latLon = LatLon.interpolate((double) i / (this.samples - 1), this.startLatLon, this.endLatLon);
}
Double elevation =
this.wwd.getModel().getGlobe().getElevation(latLon.getLatitude(), latLon.getLongitude());
this.minElevation = elevation < this.minElevation ? elevation : this.minElevation;
this.maxElevation = elevation > this.maxElevation ? elevation : this.maxElevation;
// Add position to the list
positions[i] = new Position(latLon.getLatitude(), latLon.getLongitude(), elevation);
}
// Update shape on ground
if (this.selectionShape == null)
{
this.selectionShape = new Polyline(Arrays.asList(this.positions));
this.selectionShape.setLineWidth(2);
this.selectionShape.setFollowTerrain(true);
this.selectionShape.setColor(new Color(this.color.getRed(),
this.color.getGreen(), (int) (this.color.getBlue() * .5), (int) (255 * .8)));
}
else
this.selectionShape.setPositions(Arrays.asList(this.positions));
}
else
{
// Off globe or something missing
this.positions = null;
}
}
catch (Exception e)
{
e.printStackTrace();
this.positions = null;
}
// Clear update flag
this.update = false;
}
private Position computeCursorPosition(DrawContext dc)
{
Position pos = this.wwd.getCurrentPosition();
if (pos == null && dc.getPickPoint() != null)
pos = dc.getView().computePositionFromScreenPoint(dc.getPickPoint().x, dc.getPickPoint().y);
return pos;
}
public void dispose()
{
if (this.textRenderer != null)
{
this.textRenderer.dispose();
this.textRenderer = null;
}
}
@Override
public String toString()
{
return this.getName();
}
}