Update to Worldwind release 0.4.0
[worldwind-tracker.git] / gov / nasa / worldwind / layers / Earth / TerrainProfileLayer.java
blobba1a65a5f8cc2863be32418808b2a8a2c173f1b7
1 package gov.nasa.worldwind.layers.Earth;
3 import com.sun.opengl.util.j2d.TextRenderer;
4 import gov.nasa.worldwind.WorldWindow;
5 import gov.nasa.worldwind.View;
6 import gov.nasa.worldwind.avlist.AVKey;
7 import gov.nasa.worldwind.event.*;
8 import gov.nasa.worldwind.geom.*;
9 import gov.nasa.worldwind.layers.AbstractLayer;
10 import gov.nasa.worldwind.pick.PickSupport;
11 import gov.nasa.worldwind.render.*;
12 import gov.nasa.worldwind.util.Logging;
13 import gov.nasa.worldwind.view.*;
15 import javax.media.opengl.GL;
16 import java.awt.*;
17 import java.awt.geom.*;
18 import java.beans.PropertyChangeEvent;
19 import java.util.*;
21 /**
22 * Displays a terrain profile graph in a screen corner
24 * @author Patrick Murris
25 * @version $Id$ Usage: do setEventSource(wwd) to have the graph activated and updated with position changes See public
26 * properties for options: keepProportions, follow, unit, start and end latlon...
28 public class TerrainProfileLayer extends AbstractLayer implements PositionListener
31 // Positionning constants TODO: add north, east... center-north... center-screen.
32 public final static String NORTHWEST = "gov.nasa.worldwind.TerrainProfileLayer.NorthWest";
33 public final static String SOUTHWEST = "gov.nasa.worldwind.TerrainProfileLayer.SouthWest";
34 public final static String NORTHEAST = "gov.nasa.worldwind.TerrainProfileLayer.NorthEast";
35 public final static String SOUTHEAST = "gov.nasa.worldwind.TerrainProfileLayer.SouthEast";
36 // Stretching behavior constants
37 public final static String RESIZE_STRETCH = "gov.nasa.worldwind.TerrainProfileLayer.Stretch";
38 public final static String RESIZE_SHRINK_ONLY = "gov.nasa.worldwind.TerrainProfileLayer.ShrinkOnly";
39 public final static String RESIZE_KEEP_FIXED_SIZE = "gov.nasa.worldwind.TerrainProfileLayer.FixedSize";
40 // Units constants
41 public final static String UNIT_METRIC = "gov.nasa.worldwind.TerrainProfileLayer.Metric";
42 public final static String UNIT_IMPERIAL = "gov.nasa.worldwind.TerrainProfileLayer.Imperial";
43 public final static double METER_TO_FEET = 3.280839895;
44 // Follow constants
45 public final static String FOLLOW_VIEW = "gov.nasa.worldwind.TerrainProfileLayer.FollowView";
46 public final static String FOLLOW_EYE = "gov.nasa.worldwind.TerrainProfileLayer.FollowEye";
47 public final static String FOLLOW_CURSOR = "gov.nasa.worldwind.TerrainProfileLayer.FollowCursor";
48 public final static String FOLLOW_NONE = "gov.nasa.worldwind.TerrainProfileLayer.FollowNone";
49 public final static String FOLLOW_OBJECT = "gov.nasa.worldwind.TerrainProfileLayer.FollowObject";
51 // Display parameters - TODO: make configurable
52 private Dimension size = new Dimension(250, 100);
53 private Color color = Color.white;
54 private int borderWidth = 20;
55 private String position = SOUTHWEST;
56 private String resizeBehavior = RESIZE_SHRINK_ONLY;
57 private String unit = UNIT_METRIC;
58 private Font defaultFont = Font.decode("Arial-12-PLAIN");
59 private double toViewportScale = 1;
60 private Point locationCenter = null;
61 private TextRenderer textRenderer = null;
62 private PickSupport pickSupport = new PickSupport();
63 private boolean initialized = false;
64 private boolean update = true; // Recompute profile if true
66 private int pickedSample = -1; // Picked sample number if not -1
67 Polyline selectionShape; // Shape showing section on the ground
68 Polyline pickedShape; // Shape showing actual pick position on the ground
69 private boolean keepProportions = false; // Keep graph distance/elevation proportions
70 private boolean zeroBased = true; // Pad graph elevation scale to include sea level if true
71 private String follow = FOLLOW_VIEW; // Profile position follow behavior
72 private boolean showEyePosition = false; // When FOLLOW_EYE, draw the eye position on graph when true
73 private double profileLengthFactor = 1; // Applied to default profile length (zoom on profile)
74 private LatLon startLatLon; // Section start lat/lon when FOLLOW_NONE
75 private LatLon endLatLon; // Section end lat/lon when FOLLOW_NONE
76 private Position objectPosition; // Object position if FOLLOW_OBJECT
77 private Angle objectHeading; // Object heading if FOLLOW_OBJECT
80 // Terrain profile data
81 private int samples = 250; // Number of position samples
82 private double minElevation; // Minimum elevation along the profile
83 private double maxElevation; // Maximum elevation along the profile
84 private double length; // Profile length along great circle in meter
85 private Position positions[]; // Position list
87 // Worldwind
88 private WorldWindow wwd;
90 // Draw it as ordered with an eye distance of 0 so that it shows up in front of most other things.
91 // TODO: Add general support for this common pattern.
92 private OrderedIcon orderedImage = new OrderedIcon();
94 private class OrderedIcon implements OrderedRenderable
96 public double getDistanceFromEye()
98 return 0;
101 public void pick(DrawContext dc, Point pickPoint)
103 TerrainProfileLayer.this.drawProfile(dc);
106 public void render(DrawContext dc)
108 TerrainProfileLayer.this.drawProfile(dc);
113 * Renders a terrain profile graphic in a screen corner
115 public TerrainProfileLayer()
117 this.setName(Logging.getMessage("layers.Earth.TerrainProfileLayer.Name"));
120 // ** Public properties ************************************************************
123 * Get whether the profile should be recomputed
125 * @return true if the profile should be recomputed
127 public boolean getUpdate()
129 return this.update;
133 * Set wheter the profile should be recomputed
135 * @param state true if the profile should be recomputed
137 public void setUpdate(boolean state)
139 this.update = state;
143 * Get the graphic Dimension (in pixels)
145 * @return the scalebar graphic Dimension
147 public Dimension getSize()
149 return this.size;
153 * Set the graphic Dimenion (in pixels)
155 * @param size the graphic Dimension
157 public void setSize(Dimension size)
159 if (size == null)
161 String message = Logging.getMessage("nullValue.DimensionIsNull");
162 Logging.logger().severe(message);
163 throw new IllegalArgumentException(message);
165 this.size = size;
169 * Get the graphic color
171 * @return the graphic Color
173 public Color getColor()
175 return this.color;
179 * Set the graphic Color
181 * @param color the graphic Color
183 public void setColor(Color color)
185 if (color == null)
187 String msg = Logging.getMessage("nullValue.ColorIsNull");
188 Logging.logger().severe(msg);
189 throw new IllegalArgumentException(msg);
191 this.color = color;
195 * Returns the graphic-to-viewport scale factor.
197 * @return the graphic-to-viewport scale factor
199 public double getToViewportScale()
201 return toViewportScale;
205 * Sets the scale factor applied to the viewport size to determine the displayed size of the graphic. This scale factor
206 * is used only when the layer's resize behavior is {@link #RESIZE_STRETCH} or {@link #RESIZE_SHRINK_ONLY}. The
207 * graphic's width is adjusted to occupy the proportion of the viewport's width indicated by this factor. The graphic's
208 * height is adjusted to maintain the graphic's Dimension aspect ratio.
210 * @param toViewportScale the graphic to viewport scale factor
212 public void setToViewportScale(double toViewportScale)
214 this.toViewportScale = toViewportScale;
217 public String getPosition()
219 return this.position;
223 * Sets the relative viewport location to display the graphic. Can be one of {@link #NORTHEAST} (the default), {@link
224 * #NORTHWEST}, {@link #SOUTHEAST}, or {@link #SOUTHWEST}. These indicate the corner of the viewport.
226 * @param position the desired graphic position
228 public void setPosition(String position)
230 if (position == null)
232 String msg = Logging.getMessage("nullValue.PositionIsNull");
233 Logging.logger().severe(msg);
234 throw new IllegalArgumentException(msg);
236 this.position = position;
240 * Get the screen location of the graph center if set (can be null)
242 * @return the screen location of the graph center if set (can be null)
244 public Point getLocationCenter()
246 return this.locationCenter;
250 * Set the screen location of the graph center - overrides SetPosition if not null
252 * @param point the screen location of the graph center (can be null)
254 public void setLocationCenter(Point point)
256 this.locationCenter = point;
260 * Returns the layer's resize behavior.
262 * @return the layer's resize behavior
264 public String getResizeBehavior()
266 return resizeBehavior;
270 * Sets the behavior the layer uses to size the graphic when the viewport size changes, typically when the World Wind
271 * window is resized. If the value is {@link #RESIZE_KEEP_FIXED_SIZE}, the graphic size is kept to the size specified
272 * in its Dimension scaled by the layer's current icon scale. If the value is {@link #RESIZE_STRETCH}, the graphic is
273 * resized to have a constant size relative to the current viewport size. If the viewport shrinks the graphic size
274 * decreases; if it expands then the graphic enlarges. If the value is {@link #RESIZE_SHRINK_ONLY} (the default),
275 * graphic sizing behaves as for {@link #RESIZE_STRETCH} but it will not grow larger than the size specified in its
276 * Dimension.
278 * @param resizeBehavior the desired resize behavior
280 public void setResizeBehavior(String resizeBehavior)
282 this.resizeBehavior = resizeBehavior;
285 public int getBorderWidth()
287 return borderWidth;
291 * Sets the graphic offset from the viewport border.
293 * @param borderWidth the number of pixels to offset the graphic from the borders indicated by {@link
294 * #setPosition(String)}.
296 public void setBorderWidth(int borderWidth)
298 this.borderWidth = borderWidth;
301 public String getUnit()
303 return this.unit;
307 * Sets the unit the graphic uses to display distances and elevations. Can be one of {@link #UNIT_METRIC}
308 * (the default), or {@link #UNIT_IMPERIAL}.
310 * @param unit the desired unit
312 public void setUnit(String unit)
314 this.unit = unit;
318 * Get the graphic legend Font
320 * @return the graphic legend Font
322 public Font getFont()
324 return this.defaultFont;
328 * Set the graphic legend Font
330 * @param font the graphic legend Font
332 public void setFont(Font font)
334 if (font == null)
336 String msg = Logging.getMessage("nullValue.FontIsNull");
337 Logging.logger().severe(msg);
338 throw new IllegalArgumentException(msg);
340 this.defaultFont = font;
344 * Get whether distance/elevation proportions are maintained
346 * @return true if the graph maintains distance/elevation proportions
348 public boolean getKeepProportions()
350 return this.keepProportions;
354 * Set whether distance/elevation proportions are maintained
356 * @param state true if the graph should maintains distance/elevation proportions
358 public void setKeepProportions(boolean state)
360 this.keepProportions = state;
364 * Get whether the graph center point follows the mouse cursor
366 * @return true if the graph center point follows the mouse cursor
368 public boolean getFollowCursor()
370 return this.follow.compareToIgnoreCase(FOLLOW_CURSOR) == 0;
374 * Set whether the graph center point should follows the mouse cursor
376 * @param state true if the graph center point should follows the mouse cursor
378 public void setFollowCursor(boolean state)
380 this.setFollow(state ? FOLLOW_CURSOR : FOLLOW_VIEW);
384 * Get the graph center point placement behavior
386 * @return the graph center point placement behavior
388 public String getFollow()
390 return this.follow;
394 * Set the graph center point placement behavior. Can be one of {@link #FOLLOW_VIEW} (the default),
395 * {@link #FOLLOW_CURSOR}, {@link #FOLLOW_EYE} or {@link #FOLLOW_NONE}. If {@link #FOLLOW_NONE} the profile will
396 * be computed between startLatLon and endLatLon.
398 * @param behavior the graph center point placement behavior
400 public void setFollow(String behavior)
402 this.follow = behavior;
403 this.setUpdate(true);
407 * Get whether the eye position is shown on the graph when {@link #FOLLOW_EYE}
409 * @return true if the eye position is shown on the grap
411 public boolean getShowEyePosition()
413 return this.showEyePosition;
417 * Set whether the eye position is shown on the graph when {@link #FOLLOW_EYE}
419 * @param state if true the eye position is shown on the graph
421 public void setShowEyePosition(Boolean state)
423 this.showEyePosition = state;
424 if (this.follow.compareToIgnoreCase(FOLLOW_EYE) == 0)
425 this.setUpdate(true);
429 * Set the profile length factor - has no effect if {@link #FOLLOW_NONE}
431 * @param factor the new factor
433 public void setProfileLengthFactor(double factor)
435 this.profileLengthFactor = factor;
436 this.setUpdate(true);
440 * Get the profile length factor
442 * @return the profile length factor
444 public double getProfileLenghtFactor()
446 return this.profileLengthFactor;
450 * Get the profile start position lat/lon when {@link #FOLLOW_NONE}
452 * @return the profile start position lat/lon
454 public LatLon getStartLatLon()
456 return this.startLatLon;
460 * Set the profile start position lat/lon when {@link #FOLLOW_NONE}
462 * @param latLon the profile start position lat/lon
464 public void setStartLatLon(LatLon latLon)
466 if (latLon == null)
468 String msg = Logging.getMessage("nullValue.LatLonIsNull");
469 Logging.logger().severe(msg);
470 throw new IllegalArgumentException(msg);
472 this.startLatLon = latLon;
473 if (this.follow.compareToIgnoreCase(FOLLOW_NONE) == 0)
474 this.setUpdate(true);
478 * Get the profile end position lat/lon when {@link #FOLLOW_NONE}
480 * @return the profile end position lat/lon
482 public LatLon getEndLatLon()
484 return this.endLatLon;
488 * Set the profile end position lat/lon when {@link #FOLLOW_NONE}
490 * @param latLon the profile end position lat/lon
492 public void setEndLatLon(LatLon latLon)
494 if (latLon == null)
496 String msg = Logging.getMessage("nullValue.LatLonIsNull");
497 Logging.logger().severe(msg);
498 throw new IllegalArgumentException(msg);
500 this.endLatLon = latLon;
501 if (this.follow.compareToIgnoreCase(FOLLOW_NONE) == 0)
502 this.setUpdate(true);
506 * Get the number of elevation samples in the profile
508 * @return the number of elevation samples in the profile
510 public int getSamples()
512 return this.samples;
516 * Set the number of elevation samples in the profile
518 * @param number the number of elevation samples in the profile
520 public void setSamples(int number)
522 this.samples = Math.abs(number);
523 this.setUpdate(true);
527 * Get whether the profile graph should include sea level
529 * @return whether the profile graph should include sea level
531 public boolean getZeroBased()
533 return this.zeroBased;
537 * Set whether the profile graph should include sea level. True is the default.
539 * @param state true if the profile graph should include sea level
541 public void setZeroBased(boolean state)
543 this.zeroBased = state;
544 this.setUpdate(true);
547 public Position getObjectPosition()
549 return this.objectPosition;
552 public void setObjectPosition(Position pos)
554 this.objectPosition = pos;
555 if (this.follow.compareToIgnoreCase(FOLLOW_OBJECT) == 0)
556 this.setUpdate(true);
559 public Angle getObjectHeading()
561 return this.objectHeading;
564 public void setObjectHeading(Angle heading)
566 this.objectHeading = heading;
567 if (this.follow.compareToIgnoreCase(FOLLOW_OBJECT) == 0)
568 this.setUpdate(true);
571 // ** Rendering ************************************************************
572 @Override
573 public void doRender(DrawContext dc)
575 // Delegate graph rendering to OrderedRenderable list
576 dc.addOrderedRenderable(this.orderedImage);
577 // Render section line on the ground now
578 if (this.positions != null && this.selectionShape != null)
580 this.selectionShape.render(dc);
581 // If picking in progress, render pick indicator
582 if (this.pickedSample != -1 && this.pickedShape != null)
583 this.pickedShape.render(dc);
587 @Override
588 public void doPick(DrawContext dc, Point pickPoint)
590 // No picking at the graph if selection follows the mouse cursor
591 if (this.follow.compareToIgnoreCase(FOLLOW_CURSOR) != 0)
592 // Delegate drawing to the ordered renderable list
593 dc.addOrderedRenderable(this.orderedImage);
596 private void initialize()
598 if (this.initialized || this.positions != null)
599 return;
601 if (this.wwd != null)
602 this.computeProfile();
604 if (this.positions != null)
605 this.initialized = true;
608 // Profile graph rendering - ortho
609 public void drawProfile(DrawContext dc)
611 if (this.update)
612 this.computeProfile();
614 if ((this.positions == null || (this.minElevation == 0 && this.maxElevation == 0))
615 && !this.initialized)
616 this.initialize();
618 if (this.positions == null || (this.minElevation == 0 && this.maxElevation == 0))
619 return;
621 GL gl = dc.getGL();
623 boolean attribsPushed = false;
624 boolean modelviewPushed = false;
625 boolean projectionPushed = false;
629 gl.glPushAttrib(GL.GL_DEPTH_BUFFER_BIT
630 | GL.GL_COLOR_BUFFER_BIT
631 | GL.GL_ENABLE_BIT
632 | GL.GL_TEXTURE_BIT
633 | GL.GL_TRANSFORM_BIT
634 | GL.GL_VIEWPORT_BIT
635 | GL.GL_CURRENT_BIT);
636 attribsPushed = true;
638 gl.glDisable(GL.GL_TEXTURE_2D); // no textures
640 gl.glEnable(GL.GL_BLEND);
641 gl.glBlendFunc(GL.GL_SRC_ALPHA, GL.GL_ONE_MINUS_SRC_ALPHA);
642 gl.glDisable(GL.GL_DEPTH_TEST);
644 double width = this.size.width;
645 double height = this.size.height;
647 // Load a parallel projection with xy dimensions (viewportWidth, viewportHeight)
648 // into the GL projection matrix.
649 java.awt.Rectangle viewport = dc.getView().getViewport();
650 gl.glMatrixMode(javax.media.opengl.GL.GL_PROJECTION);
651 gl.glPushMatrix();
652 projectionPushed = true;
653 gl.glLoadIdentity();
654 double maxwh = width > height ? width : height;
655 gl.glOrtho(0d, viewport.width, 0d, viewport.height, -0.6 * maxwh, 0.6 * maxwh);
657 gl.glMatrixMode(GL.GL_MODELVIEW);
658 gl.glPushMatrix();
659 modelviewPushed = true;
660 gl.glLoadIdentity();
662 // Scale to a width x height space
663 // located at the proper position on screen
664 double scale = this.computeScale(viewport);
665 Vec4 locationSW = this.computeLocation(viewport, scale);
666 gl.glTranslated(locationSW.x(), locationSW.y(), locationSW.z());
667 gl.glScaled(scale, scale, 1d);
669 if (!dc.isPickingMode())
671 // Draw grid - Set color using current layer opacity
672 this.drawGrid(dc, this.size);
674 // Draw profile graph
675 this.drawGraph(dc, this.size);
677 // Draw labels
678 String label = String.format("min %.0fm max %.0fm", this.minElevation, this.maxElevation);
679 if (this.unit.equals(UNIT_IMPERIAL))
680 label = String.format("min %.0fft max %.0fft", this.minElevation * METER_TO_FEET,
681 this.maxElevation * METER_TO_FEET);
682 gl.glLoadIdentity();
683 gl.glDisable(GL.GL_CULL_FACE);
684 drawLabel(label, locationSW.add3(new Vec4(0, -12, 0)), -1); // left aligned
685 if (this.pickedSample != -1)
687 double pickedElevation = positions[this.pickedSample].getElevation();
688 label = String.format("%.0fm", pickedElevation);
689 if (this.unit.equals(UNIT_IMPERIAL))
690 label = String.format("%.0fft", pickedElevation * METER_TO_FEET);
691 drawLabel(label, locationSW.add3(new Vec4(width, -12, 0)), 1); // right aligned
694 else
696 // Picking
697 this.pickSupport.clearPickList();
698 this.pickSupport.beginPicking(dc);
699 // Where in the world are we picking ?
700 Position pickPosition =
701 computePickPosition(dc, locationSW, new Dimension((int) (width * scale), (int) (height * scale)));
702 // Draw unique color across the rectangle
703 Color color = dc.getUniquePickColor();
704 int colorCode = color.getRGB();
705 // Add our object(s) to the pickable list
706 this.pickSupport.addPickableObject(colorCode, this, pickPosition, false);
707 gl.glScaled(width, height, 1d);
708 gl.glColor3ub((byte) color.getRed(), (byte) color.getGreen(), (byte) color.getBlue());
709 gl.glBegin(GL.GL_POLYGON);
710 gl.glVertex3d(0, 0, 0);
711 gl.glVertex3d(1, 0, 0);
712 gl.glVertex3d(1, 1, 0);
713 gl.glVertex3d(0, 1, 0);
714 gl.glVertex3d(0, 0, 0);
715 gl.glEnd();
716 // Done picking
717 this.pickSupport.endPicking(dc);
718 this.pickSupport.resolvePick(dc, dc.getPickPoint(), this);
722 finally
724 if (projectionPushed)
726 gl.glMatrixMode(GL.GL_PROJECTION);
727 gl.glPopMatrix();
729 if (modelviewPushed)
731 gl.glMatrixMode(GL.GL_MODELVIEW);
732 gl.glPopMatrix();
734 if (attribsPushed)
735 gl.glPopAttrib();
739 // Draw grid graphic
740 private void drawGrid(DrawContext dc, Dimension dimension)
742 // Background color
743 Color backColor = getBackgroundColor(this.color);
744 drawFilledRectangle(dc, new Vec4(0, 0, 0), dimension, new Color(backColor.getRed(),
745 backColor.getGreen(), backColor.getBlue(), (int) (backColor.getAlpha() * .5))); // Increased transparency
746 // Grid - minimal
747 float[] colorRGB = this.color.getRGBColorComponents(null);
748 dc.getGL().glColor4d(colorRGB[0], colorRGB[1], colorRGB[2], this.getOpacity());
749 drawVerticalLine(dc, dimension, 0);
750 drawVerticalLine(dc, dimension, dimension.getWidth());
751 drawHorizontalLine(dc, dimension, 0);
754 // Draw profile graphic
755 private void drawGraph(DrawContext dc, Dimension dimension)
757 GL gl = dc.getGL();
758 // Adjust min/max elevation for the graph
759 double min = this.minElevation;
760 double max = this.maxElevation;
761 if (this.showEyePosition && this.follow.compareToIgnoreCase(FOLLOW_EYE) == 0)
762 max = Math.max(max, dc.getView().getEyePosition().getElevation());
763 if (this.showEyePosition && this.follow.compareToIgnoreCase(FOLLOW_OBJECT) == 0 && this.objectPosition != null)
764 max = Math.max(max, this.objectPosition.getElevation());
765 if (this.zeroBased)
767 if (min > 0) min = 0;
768 if (max < 0) max = 0;
770 int i;
771 double stepX = dimension.getWidth() / (this.length);
772 double stepY = dimension.getHeight() / (max - min);
773 if (this.keepProportions)
775 stepX = Math.min(stepX, stepY);
776 stepY = stepX;
778 double lengthStep = this.length / (this.samples - 1);
779 double x = 0, y = 0;
780 // Filled graph
781 gl.glColor4ub((byte) this.color.getRed(), (byte) this.color.getGreen(),
782 (byte) this.color.getBlue(), (byte) 100);
783 gl.glBegin(GL.GL_TRIANGLE_STRIP);
784 for (i = 0; i < this.samples; i++)
786 x = i * lengthStep * stepX;
787 y = (this.positions[i].getElevation() - min) * stepY;
788 gl.glVertex3d(x, 0, 0);
789 gl.glVertex3d(x, y, 0);
791 gl.glEnd();
792 // Line graph
793 float[] colorRGB = this.color.getRGBColorComponents(null);
794 gl.glColor4d(colorRGB[0], colorRGB[1], colorRGB[2], this.getOpacity());
795 gl.glBegin(GL.GL_LINE_STRIP);
796 for (i = 0; i < this.samples; i++)
798 x = i * lengthStep * stepX;
799 y = (this.positions[i].getElevation() - min) * stepY;
800 gl.glVertex3d(x, y, 0);
802 gl.glEnd();
803 // Middle vertical line
804 gl.glColor4d(colorRGB[0], colorRGB[1], colorRGB[2], this.getOpacity() * .3); // increased transparency here
805 drawVerticalLine(dc, dimension, x / 2);
806 // Eye position
807 if ((this.follow.compareToIgnoreCase(FOLLOW_EYE) == 0 ||
808 (this.follow.compareToIgnoreCase(FOLLOW_OBJECT) == 0 && this.objectPosition != null))
809 && this.showEyePosition)
811 double eyeY = (dc.getView().getEyePosition().getElevation() - min) * stepY;
812 if (this.follow.compareToIgnoreCase(FOLLOW_OBJECT) == 0)
813 eyeY = (this.objectPosition.getElevation() - min) * stepY;
814 this.drawFilledRectangle(dc, new Vec4(x / 2 - 2, eyeY - 2, 0), new Dimension(5, 5), this.color);
816 // Selected/picked vertical and horizontal lines
817 if (this.pickedSample != -1)
819 double pickedX = this.pickedSample * lengthStep * stepX;
820 double pickedY = (positions[this.pickedSample].getElevation() - min) * stepY;
821 gl.glColor4d(colorRGB[0], colorRGB[1], colorRGB[2] * .5, this.getOpacity() * .8); // yellower color
822 drawVerticalLine(dc, dimension, pickedX);
823 drawHorizontalLine(dc, dimension, pickedY);
824 // Eye or object - picked position line
825 if ((this.follow.compareToIgnoreCase(FOLLOW_EYE) == 0 ||
826 (this.follow.compareToIgnoreCase(FOLLOW_OBJECT) == 0 && this.objectPosition != null))
827 && this.showEyePosition)
829 // Line
830 double eyeY = (dc.getView().getEyePosition().getElevation() - min) * stepY;
831 if (this.follow.compareToIgnoreCase(FOLLOW_OBJECT) == 0)
832 eyeY = (this.objectPosition.getElevation() - min) * stepY;
833 drawLine(dc, pickedX, pickedY, x / 2, eyeY);
834 // Distance label
835 double distance = dc.getView().getEyePoint().distanceTo3(
836 dc.getGlobe().computePointFromPosition(positions[this.pickedSample]));
837 if (this.follow.compareToIgnoreCase(FOLLOW_OBJECT) == 0)
838 distance = dc.getGlobe().computePointFromPosition(this.objectPosition).distanceTo3(
839 dc.getGlobe().computePointFromPosition(positions[this.pickedSample]));
840 String label = String.format("Dist %.0fm", distance);
841 if (this.unit.equals(UNIT_IMPERIAL))
842 label = String.format("Dist %.0fft", distance * METER_TO_FEET);
843 drawLabel(label, new Vec4(pickedX + 5, pickedY - 12, 0), -1); // left aligned
846 // Min elevation horizontal line
847 if (this.minElevation != min)
849 y = (this.minElevation - min) * stepY;
850 gl.glColor4d(colorRGB[0], colorRGB[1], colorRGB[2], this.getOpacity() * .5); // medium transparency
851 drawHorizontalLine(dc, dimension, y);
853 // Max elevation horizontal line
854 if (this.maxElevation != max)
856 y = (this.maxElevation - min) * stepY;
857 gl.glColor4d(colorRGB[0], colorRGB[1], colorRGB[2], this.getOpacity() * .5); // medium transparency
858 drawHorizontalLine(dc, dimension, y);
860 // Sea level in between positive elevations only (not across land)
861 if (min < 0 && max >= 0)
863 gl.glColor4d(colorRGB[0] * .7, colorRGB[1] * .7, colorRGB[2], this.getOpacity() * .5); // bluer color
864 y = -this.minElevation * stepY;
865 double previousX = -1;
866 for (i = 0; i < this.samples; i++)
868 x = i * lengthStep * stepX;
869 if (this.positions[i].getElevation() > 0 || i == this.samples - 1)
871 if (previousX >= 0)
873 gl.glBegin(GL.GL_LINE_STRIP);
874 gl.glVertex3d(previousX, y, 0);
875 gl.glVertex3d(x, y, 0);
876 gl.glEnd();
877 previousX = -1;
880 else
881 previousX = previousX < 0 ? x : previousX;
886 private void drawHorizontalLine(DrawContext dc, Dimension dimension, double y)
888 drawLine(dc, 0, y, dimension.getWidth(), y);
891 private void drawVerticalLine(DrawContext dc, Dimension dimension, double x)
893 drawLine(dc, x, 0, x, dimension.getHeight());
896 private void drawFilledRectangle(DrawContext dc, Vec4 origin, Dimension dimension, Color color)
898 GL gl = dc.getGL();
899 gl.glColor4ub((byte) color.getRed(), (byte) color.getGreen(),
900 (byte) color.getBlue(), (byte) color.getAlpha());
901 gl.glDisable(GL.GL_TEXTURE_2D); // no textures
902 gl.glBegin(GL.GL_POLYGON);
903 gl.glVertex3d(origin.x, origin.y, 0);
904 gl.glVertex3d(origin.x + dimension.getWidth(), origin.y, 0);
905 gl.glVertex3d(origin.x + dimension.getWidth(), origin.y + dimension.getHeight(), 0);
906 gl.glVertex3d(origin.x, origin.y + dimension.getHeight(), 0);
907 gl.glVertex3d(origin.x, origin.y, 0);
908 gl.glEnd();
912 private void drawLine(DrawContext dc, double x1, double y1, double x2, double y2)
914 GL gl = dc.getGL();
915 gl.glBegin(GL.GL_LINE_STRIP);
916 gl.glVertex3d(x1, y1, 0);
917 gl.glVertex3d(x2, y2, 0);
918 gl.glEnd();
921 // Draw a text label
922 // Align = -1: left, 0: center and 1: right
923 private void drawLabel(String text, Vec4 screenPoint, int align)
925 if (this.textRenderer == null)
927 this.textRenderer = new TextRenderer(this.defaultFont, true, true);
930 Rectangle2D nameBound = this.textRenderer.getBounds(text);
931 int x = (int) screenPoint.x(); // left
932 if (align == 0) x = (int) (screenPoint.x() - nameBound.getWidth() / 2d); // centered
933 if (align > 0) x = (int) (screenPoint.x() - nameBound.getWidth()); // right
934 int y = (int) screenPoint.y();
936 this.textRenderer.begin3DRendering();
938 this.textRenderer.setColor(this.getBackgroundColor(this.color));
939 this.textRenderer.draw(text, x + 1, y - 1);
940 this.textRenderer.setColor(this.color);
941 this.textRenderer.draw(text, x, y);
943 this.textRenderer.end3DRendering();
947 // Compute background color for best contrast
948 private Color getBackgroundColor(Color color)
950 float[] compArray = new float[4];
951 Color.RGBtoHSB(color.getRed(), color.getGreen(), color.getBlue(), compArray);
952 if (compArray[2] > 0.5)
953 return new Color(0, 0, 0, (int) (this.color.getAlpha() * 0.7f));
954 else
955 return new Color(255, 255, 255, (int) (this.color.getAlpha() * 0.7f));
958 // ** Dimensions and positionning ************************************************************
960 private double computeScale(java.awt.Rectangle viewport)
962 if (this.resizeBehavior.equals(RESIZE_SHRINK_ONLY))
964 return Math.min(1d, (this.toViewportScale) * viewport.width / this.size.width);
966 else if (this.resizeBehavior.equals(RESIZE_STRETCH))
968 return (this.toViewportScale) * viewport.width / this.size.width;
970 else if (this.resizeBehavior.equals(RESIZE_KEEP_FIXED_SIZE))
972 return 1d;
974 else
976 return 1d;
980 private Vec4 computeLocation(java.awt.Rectangle viewport, double scale)
982 double scaledWidth = scale * this.size.width;
983 double scaledHeight = scale * this.size.height;
985 double x;
986 double y;
988 if (this.locationCenter != null)
990 x = this.locationCenter.x - scaledWidth / 2;
991 y = this.locationCenter.y - scaledHeight / 2;
993 else if (this.position.equals(NORTHEAST))
995 x = viewport.getWidth() - scaledWidth - this.borderWidth;
996 y = viewport.getHeight() - scaledHeight - this.borderWidth;
998 else if (this.position.equals(SOUTHEAST))
1000 x = viewport.getWidth() - scaledWidth - this.borderWidth;
1001 y = 0d + this.borderWidth;
1003 else if (this.position.equals(NORTHWEST))
1005 x = 0d + this.borderWidth;
1006 y = viewport.getHeight() - scaledHeight - this.borderWidth;
1008 else if (this.position.equals(SOUTHWEST))
1010 x = 0d + this.borderWidth;
1011 y = 0d + this.borderWidth;
1013 else // use North East
1015 x = viewport.getWidth() - scaledWidth / 2 - this.borderWidth;
1016 y = viewport.getHeight() - scaledHeight / 2 - this.borderWidth;
1019 return new Vec4(x, y, 0);
1022 private Position computeGroundPosition(DrawContext dc, View view)
1024 if (view == null)
1025 return null;
1027 Position groundPos = view.computePositionFromScreenPoint(
1028 view.getViewport().getWidth() / 2, view.getViewport().getHeight() / 2);
1029 if (groundPos == null)
1030 return null;
1032 double elevation = dc.getGlobe().getElevation(groundPos.getLatitude(), groundPos.getLongitude());
1033 return new Position(
1034 groundPos.getLatitude(),
1035 groundPos.getLongitude(),
1036 elevation * dc.getVerticalExaggeration());
1040 * Computes the Position of the pickPoint over the graph and updates pickedSample indice
1042 * @param dc the current DrawContext
1043 * @param locationSW the screen location of the bottom left corner of the graph
1044 * @param mapSize the graph screen dimension in pixels
1045 * @return the picked Position
1047 private Position computePickPosition(DrawContext dc, Vec4 locationSW, Dimension mapSize)
1049 Position pickPosition = null;
1050 this.pickedSample = -1;
1051 Point pickPoint = dc.getPickPoint();
1052 if (pickPoint != null && this.positions != null)
1054 Rectangle viewport = dc.getView().getViewport();
1055 // Check if pickpoint is inside the graph
1056 if (pickPoint.getX() >= locationSW.getX()
1057 && pickPoint.getX() < locationSW.getX() + mapSize.width
1058 && viewport.height - pickPoint.getY() >= locationSW.getY()
1059 && viewport.height - pickPoint.getY() < locationSW.getY() + mapSize.height)
1061 // Find sample - Note: only works when graph expends over the full width
1062 int sample = (int) (((double) (pickPoint.getX() - locationSW.getX()) / mapSize.width) * this.samples);
1063 if (sample >= 0 && sample < this.samples)
1065 pickPosition = this.positions[sample];
1066 this.pickedSample = sample;
1067 // Update polyline indicator
1068 ArrayList<Position> posList = new ArrayList<Position>();
1069 posList.add(positions[sample]);
1070 posList.add(new Position(positions[sample].getLatitude(), positions[sample].getLongitude(),
1071 positions[sample].getElevation() + this.length / 10));
1072 if (this.pickedShape == null)
1074 this.pickedShape = new Polyline(posList);
1075 this.pickedShape.setPathType(Polyline.LINEAR);
1076 this.pickedShape.setLineWidth(2);
1077 this.pickedShape.setColor(new Color(this.color.getRed(),
1078 this.color.getGreen(), (int) (this.color.getBlue() * .8), (int) (255 * .8)));
1080 else
1081 this.pickedShape.setPositions(posList);
1085 return pickPosition;
1088 // ** Position listener impl. ************************************************************
1090 public void moved(PositionEvent event)
1092 if (this.wwd != null && this.isEnabled())
1094 // Update profile if FOLLOW_CURSOR
1095 if (this.follow.compareToIgnoreCase(FOLLOW_CURSOR) == 0)
1096 this.setUpdate(true);
1098 else
1099 this.positions = null;
1102 // ** Property change listener ***********************************************************
1104 public void propertyChange(PropertyChangeEvent propertyChangeEvent)
1106 if (this.wwd != null && this.isEnabled())
1108 // Update profile if FOLLOW_VIEW or FOLLOW_EYE or terrain elevations changed
1109 if (this.follow.compareToIgnoreCase(FOLLOW_VIEW) == 0
1110 || this.follow.compareToIgnoreCase(FOLLOW_EYE) == 0
1111 || propertyChangeEvent.getPropertyName().compareToIgnoreCase(AVKey.ELEVATION_MODEL) == 0)
1112 this.setUpdate(true);
1114 else
1115 this.positions = null;
1118 // Sets the wwd local reference and add us to the position listeners
1119 // the view and elevation model property change listener
1120 public void setEventSource(WorldWindow wwd)
1122 if (this.wwd != null)
1124 this.wwd.removePositionListener(this);
1125 this.wwd.getView().removePropertyChangeListener(this);
1126 this.wwd.getModel().getGlobe().getElevationModel().removePropertyChangeListener(this);
1128 this.wwd = wwd;
1129 if (this.wwd != null)
1131 this.wwd.addPositionListener(this);
1132 this.wwd.getView().addPropertyChangeListener(this);
1133 this.wwd.getModel().getGlobe().getElevationModel().addPropertyChangeListener(this);
1137 // ** Profile data collection ************************************************************
1140 * If {@link #FOLLOW_VIEW ], {@link #FOLLOW_EYE] or {@link #FOLLOW_CURSOR] collects terrain profile data along a
1141 * great circle line centered at the current position (view, eye or cursor) and perpendicular to the view heading.
1142 * If {@link #FOLLOW_NONE] the profile is computed in between start and end latlon
1144 public void computeProfile()
1146 if (this.wwd == null)
1147 return;
1150 // Find center position
1151 OrbitView view = (OrbitView) this.wwd.getView(); // TODO: check for OrbitView instance first
1152 Position groundPos = view.computePositionFromScreenPoint(
1153 view.getViewport().getWidth() / 2, view.getViewport().getHeight() / 2);
1154 if (view.getLookAtLatitude() != null && view.getLookAtLongitude() != null)
1155 groundPos = new Position(view.getLookAtLatitude(), view.getLookAtLongitude(), 0);
1156 if (this.follow.compareToIgnoreCase(FOLLOW_CURSOR) == 0)
1157 groundPos = this.wwd.getCurrentPosition();
1158 if (this.follow.compareToIgnoreCase(FOLLOW_EYE) == 0)
1159 groundPos = view.getEyePosition();
1160 if (this.follow.compareToIgnoreCase(FOLLOW_OBJECT) == 0)
1161 groundPos = this.objectPosition;
1162 if ((this.follow.compareToIgnoreCase(FOLLOW_VIEW) == 0 && groundPos != null ) ||
1163 (this.follow.compareToIgnoreCase(FOLLOW_EYE) == 0 && groundPos != null ) ||
1164 (this.follow.compareToIgnoreCase(FOLLOW_CURSOR) == 0 && groundPos != null ) ||
1165 (this.follow.compareToIgnoreCase(FOLLOW_NONE) == 0 && this.startLatLon != null && this.endLatLon != null) ||
1166 (this.follow.compareToIgnoreCase(FOLLOW_OBJECT) == 0 && this.objectPosition != null && this.objectHeading != null))
1168 this.positions = new Position[samples];
1169 this.minElevation = Double.MAX_VALUE;
1170 this.maxElevation = -1e6; // Note: Double.MIN_VALUE would fail below min-max tests for some reason
1171 this.length = Math.min(view.getZoom() * .8 * this.profileLengthFactor,
1172 this.wwd.getModel().getGlobe().getRadius() * Math.PI);
1173 if (this.follow.compareToIgnoreCase(FOLLOW_NONE) == 0)
1174 this.length = LatLon.sphericalDistance(this.startLatLon, this.endLatLon).radians
1175 * this.wwd.getModel().getGlobe().getRadius();
1176 if (this.follow.compareToIgnoreCase(FOLLOW_OBJECT) == 0)
1177 this.length = Math.min(this.objectPosition.getElevation() * .8 * this.profileLengthFactor,
1178 this.wwd.getModel().getGlobe().getRadius() * Math.PI);
1179 double lengthRadian = this.length / this.wwd.getModel().getGlobe().getRadius();
1181 LatLon centerLatLon = new LatLon(groundPos.getLatitude(), groundPos.getLongitude());
1182 // Iterate on both sides of the center point
1183 int i;
1184 double step = lengthRadian / (samples - 1);
1185 for (i = 0; i < this.samples; i++)
1187 LatLon latLon = null;
1188 if (this.follow.compareToIgnoreCase(FOLLOW_NONE) != 0)
1190 // Compute segments perpendicular to view or object heading
1191 double azimuth = view.getHeading().subtract(Angle.POS90).radians;
1192 if (this.follow.compareToIgnoreCase(FOLLOW_OBJECT) == 0)
1193 azimuth = this.objectHeading.subtract(Angle.POS90).radians;
1194 if (i > (float) (this.samples - 1) / 2f)
1196 //azimuth = view.getHeading().subtract(Angle.NEG90).radians;
1197 azimuth += Math.PI;
1199 double distance = Math.abs(((double) i - ((double) (this.samples - 1) / 2d)) * step);
1200 latLon = LatLon.endPosition(centerLatLon, azimuth, distance);
1202 else if (this.follow.compareToIgnoreCase(FOLLOW_NONE) == 0 && this.startLatLon != null
1203 && this.endLatLon != null)
1205 // Compute segments between start and end positions latlon
1206 latLon = LatLon.interpolate((double) i / (this.samples - 1), this.startLatLon, this.endLatLon);
1208 Double elevation =
1209 this.wwd.getModel().getGlobe().getElevation(latLon.getLatitude(), latLon.getLongitude());
1210 this.minElevation = elevation < this.minElevation ? elevation : this.minElevation;
1211 this.maxElevation = elevation > this.maxElevation ? elevation : this.maxElevation;
1212 // Add position to the list
1213 positions[i] = new Position(latLon.getLatitude(), latLon.getLongitude(), elevation);
1215 // Update shape on ground
1216 if (this.selectionShape == null)
1218 this.selectionShape = new Polyline(Arrays.asList(this.positions));
1219 this.selectionShape.setLineWidth(2);
1220 this.selectionShape.setFollowTerrain(true);
1221 this.selectionShape.setColor(new Color(this.color.getRed(),
1222 this.color.getGreen(), (int) (this.color.getBlue() * .5), (int) (255 * .8)));
1224 else
1225 this.selectionShape.setPositions(Arrays.asList(this.positions));
1227 else
1229 // Off globe or something missing
1230 this.positions = null;
1233 catch (Exception e)
1235 this.positions = null;
1237 // Clear update flag
1238 this.update = false;
1241 public void dispose()
1243 if (this.textRenderer != null)
1245 this.textRenderer.dispose();
1246 this.textRenderer = null;
1250 @Override
1251 public String toString()
1253 return this.getName();