src.gov.nasa.worldwind.render.Polyline Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of worldwindx Show documentation
Show all versions of worldwindx Show documentation
World Wind is a collection of components that interactively display 3D geographic information within Java applications or applets.
/*
* Copyright (C) 2012 United States Government as represented by the Administrator of the
* National Aeronautics and Space Administration.
* All Rights Reserved.
*/
package gov.nasa.worldwind.render;
import gov.nasa.worldwind.*;
import gov.nasa.worldwind.avlist.*;
import gov.nasa.worldwind.geom.*;
import gov.nasa.worldwind.geom.Cylinder;
import gov.nasa.worldwind.globes.Globe;
import gov.nasa.worldwind.layers.Layer;
import gov.nasa.worldwind.pick.PickSupport;
import gov.nasa.worldwind.util.*;
import gov.nasa.worldwind.util.measure.LengthMeasurer;
import javax.media.opengl.*;
import java.awt.*;
import java.util.*;
import java.util.List;
/**
* @author tag
* @version $Id: Polyline.java 1171 2013-02-11 21:45:02Z dcollins $
*/
public class Polyline extends AVListImpl implements Renderable, OrderedRenderable, Movable, Restorable,
MeasurableLength, ExtentHolder
{
public final static int GREAT_CIRCLE = WorldWind.GREAT_CIRCLE;
public final static int LINEAR = WorldWind.LINEAR;
public final static int RHUMB_LINE = WorldWind.RHUMB_LINE;
public final static int LOXODROME = RHUMB_LINE;
public final static int ANTIALIAS_DONT_CARE = WorldWind.ANTIALIAS_DONT_CARE;
public final static int ANTIALIAS_FASTEST = WorldWind.ANTIALIAS_FASTEST;
public final static int ANTIALIAS_NICEST = WorldWind.ANTIALIAS_NICEST;
protected ArrayList positions;
protected Vec4 referenceCenterPoint;
protected int antiAliasHint = GL.GL_FASTEST;
protected Color color = Color.WHITE;
protected double lineWidth = 1;
protected boolean filled = false; // makes it a polygon
protected boolean closed = false; // connect last point to first
protected boolean followTerrain = false;
protected double offset = 0;
protected double terrainConformance = 10;
protected int pathType = GREAT_CIRCLE;
protected List> currentSpans;
protected short stipplePattern = (short) 0xAAAA;
protected int stippleFactor = 0;
protected int numSubsegments = 10;
protected boolean highlighted = false;
protected Color highlightColor = new Color(1f, 1f, 1f, 0.5f);
protected Object delegateOwner;
protected LengthMeasurer measurer = new LengthMeasurer();
protected long geomGenTimeStamp = -Long.MAX_VALUE;
protected double geomGenVE = 1;
protected double eyeDistance;
protected PickSupport pickSupport = new PickSupport();
protected long frameNumber = -1; // identifies frame used to calculate these values
protected Layer pickLayer;
// Manage an extent for each globe the polyline's associated with.
protected static class ExtentInfo
{
// The extent depends on the state of the globe used to compute it, and the vertical exaggeration.
protected Extent extent;
protected double verticalExaggeration;
protected Globe globe;
protected Object globeStateKey;
public ExtentInfo(Extent extent, DrawContext dc)
{
this.extent = extent;
this.verticalExaggeration = dc.getVerticalExaggeration();
this.globe = dc.getGlobe();
this.globeStateKey = dc.getGlobe().getStateKey(dc);
}
protected boolean isValid(DrawContext dc)
{
return this.verticalExaggeration == dc.getVerticalExaggeration() && this.globe == dc.getGlobe()
&& globeStateKey.equals(dc.getGlobe().getStateKey(dc));
}
}
protected HashMap extents = new HashMap(2); // usually only 1, but few at most
public Polyline()
{
this.setPositions(null);
this.measurer.setFollowTerrain(this.followTerrain);
this.measurer.setPathType(this.pathType);
}
public Polyline(Iterable positions)
{
this.setPositions(positions);
this.measurer.setFollowTerrain(this.followTerrain);
this.measurer.setPathType(this.pathType);
}
public Polyline(Iterable positions, double elevation)
{
this.setPositions(positions, elevation);
this.measurer.setFollowTerrain(this.followTerrain);
this.measurer.setPathType(this.pathType);
}
private void reset()
{
if (this.currentSpans != null)
this.currentSpans.clear();
this.currentSpans = null;
}
public Color getColor()
{
return 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;
}
public int getAntiAliasHint()
{
return antiAliasHint;
}
public void setAntiAliasHint(int hint)
{
if (!(hint == ANTIALIAS_DONT_CARE || hint == ANTIALIAS_FASTEST || hint == ANTIALIAS_NICEST))
{
String msg = Logging.getMessage("generic.InvalidHint");
Logging.logger().severe(msg);
throw new IllegalArgumentException(msg);
}
this.antiAliasHint = hint;
}
public boolean isFilled()
{
return filled;
}
public void setFilled(boolean filled)
{
this.filled = filled;
}
public int getPathType()
{
return pathType;
}
public String getPathTypeString()
{
return this.getPathType() == GREAT_CIRCLE ? AVKey.GREAT_CIRCLE
: this.getPathType() == RHUMB_LINE ? AVKey.RHUMB_LINE : AVKey.LINEAR;
}
/**
* Sets the type of path to draw, one of {@link #GREAT_CIRCLE}, which draws each segment of the path as a great
* circle, {@link #LINEAR}, which determines the intermediate positions between segments by interpolating the
* segment endpoints, or {@link #RHUMB_LINE}, which draws each segment of the path as a line of constant heading.
*
* @param pathType the type of path to draw.
*
* @see Path Types
*/
public void setPathType(int pathType)
{
this.reset();
this.pathType = pathType;
this.measurer.setPathType(pathType);
}
/**
* Sets the type of path to draw, one of {@link AVKey#GREAT_CIRCLE}, which draws each segment of the path as a great
* circle, {@link AVKey#LINEAR}, which determines the intermediate positions between segments by interpolating the
* segment endpoints, or {@link AVKey#RHUMB_LINE}, which draws each segment of the path as a line of constant
* heading.
*
* @param pathType the type of path to draw.
*
* @see Path Types
*/
public void setPathType(String pathType)
{
if (pathType == null)
{
String msg = Logging.getMessage("nullValue.PathTypeIsNull");
Logging.logger().severe(msg);
throw new IllegalArgumentException(msg);
}
this.setPathType(pathType.equals(AVKey.GREAT_CIRCLE) ? GREAT_CIRCLE
: pathType.equals(AVKey.RHUMB_LINE) || pathType.equals(AVKey.LOXODROME) ? RHUMB_LINE : LINEAR);
}
public boolean isFollowTerrain()
{
return followTerrain;
}
/**
* Indicates whether the path should follow the terrain's surface. If the value is true
, the elevation
* values in this path's positions are ignored and the path is drawn on the terrain surface. Otherwise the path is
* drawn according to the elevations given in the path's positions. If following the terrain, the path may also have
* an offset. See {@link #setOffset(double)};
*
* @param followTerrain true
to follow the terrain, otherwise false
.
*/
public void setFollowTerrain(boolean followTerrain)
{
this.reset();
this.followTerrain = followTerrain;
this.measurer.setFollowTerrain(followTerrain);
this.extents.clear();
}
public double getOffset()
{
return offset;
}
/**
* Specifies an offset, in meters, to add to the path points when the path's follow-terrain attribute is true. See
* {@link #setFollowTerrain(boolean)}.
*
* @param offset the path pffset in meters.
*/
public void setOffset(double offset)
{
this.reset();
this.offset = offset;
this.extents.clear();
}
public double getTerrainConformance()
{
return terrainConformance;
}
/**
* Specifies the precision to which the path follows the terrain when the follow-terrain attribute is true. The
* conformance value indicates the approximate length of each sub-segment of the path as it's drawn, in pixels.
* Lower values specify higher precision, but at the cost of performance.
*
* @param terrainConformance the path conformance in pixels.
*/
public void setTerrainConformance(double terrainConformance)
{
this.terrainConformance = terrainConformance;
}
public double getLineWidth()
{
return lineWidth;
}
public void setLineWidth(double lineWidth)
{
this.lineWidth = lineWidth;
}
/**
* Returns the length of the line as drawn. If the path follows the terrain, the length returned is the distance one
* would travel if on the surface. If the path does not follow the terrain, the length returned is the distance
* along the full length of the path at the path's elevations and current path type.
*
* @return the path's length in meters.
*/
public double getLength()
{
Iterator infos = this.extents.values().iterator();
return infos.hasNext() ? this.measurer.getLength(infos.next().globe) : 0;
}
public double getLength(Globe globe)
{
// The length measurer will throw an exception and log the error if globe is null
return this.measurer.getLength(globe);
}
public LengthMeasurer getMeasurer()
{
return this.measurer;
}
public short getStipplePattern()
{
return stipplePattern;
}
/**
* Sets the stipple pattern for specifying line types other than solid. See the OpenGL specification or programming
* guides for a description of this parameter. Stipple is also affected by the path's stipple factor, {@link
* #setStippleFactor(int)}.
*
* @param stipplePattern the stipple pattern.
*/
public void setStipplePattern(short stipplePattern)
{
this.stipplePattern = stipplePattern;
}
public int getStippleFactor()
{
return stippleFactor;
}
/**
* Sets the stipple factor for specifying line types other than solid. See the OpenGL specification or programming
* guides for a description of this parameter. Stipple is also affected by the path's stipple pattern, {@link
* #setStipplePattern(short)}.
*
* @param stippleFactor the stipple factor.
*/
public void setStippleFactor(int stippleFactor)
{
this.stippleFactor = stippleFactor;
}
public int getNumSubsegments()
{
return numSubsegments;
}
/**
* Specifies the number of intermediate segments to draw for each segment between positions. The end points of the
* intermediate segments are calculated according to the current path type and follow-terrain setting.
*
* @param numSubsegments the number of intermediate subsegments.
*/
public void setNumSubsegments(int numSubsegments)
{
this.reset();
this.numSubsegments = numSubsegments;
}
public boolean isHighlighted()
{
return highlighted;
}
public void setHighlighted(boolean highlighted)
{
this.highlighted = highlighted;
}
public Color getHighlightColor()
{
return this.highlightColor;
}
public void setHighlightColor(Color highlightColor)
{
if (highlightColor == null)
{
String message = Logging.getMessage("nullValue.ColorIsNull");
Logging.logger().severe(message);
throw new IllegalStateException(message);
}
this.highlightColor = highlightColor;
}
/**
* Specifies the path's positions.
*
* @param inPositions the path positions.
*/
public void setPositions(Iterable inPositions)
{
this.reset();
this.positions = new ArrayList();
this.extents.clear();
if (inPositions != null)
{
for (Position position : inPositions)
{
this.positions.add(position);
}
this.measurer.setPositions(this.positions);
}
if ((this.filled && this.positions.size() < 3))
{
String msg = Logging.getMessage("generic.InsufficientPositions");
Logging.logger().severe(msg);
throw new IllegalArgumentException(msg);
}
}
/**
* Sets the paths positions as latitude and longitude values at a constant altitude.
*
* @param inPositions the latitudes and longitudes of the positions.
* @param altitude the elevation to assign each position.
*/
public void setPositions(Iterable inPositions, double altitude)
{
this.reset();
this.positions = new ArrayList();
this.extents.clear();
if (inPositions != null)
{
for (LatLon position : inPositions)
{
this.positions.add(new Position(position, altitude));
}
this.measurer.setPositions(this.positions);
}
if (this.filled && this.positions.size() < 3)
{
String msg = Logging.getMessage("generic.InsufficientPositions");
Logging.logger().severe(msg);
throw new IllegalArgumentException(msg);
}
}
public Iterable getPositions()
{
return this.positions;
}
public boolean isClosed()
{
return closed;
}
public void setClosed(boolean closed)
{
this.closed = closed;
}
/**
* Returns the delegate owner of this Polyline. If non-null, the returned object replaces the Polyline as the
* pickable object returned during picking. If null, the Polyline itself is the pickable object returned during
* picking.
*
* @return the object used as the pickable object returned during picking, or null to indicate that the Polyline is
* returned during picking.
*/
public Object getDelegateOwner()
{
return this.delegateOwner;
}
/**
* Specifies the delegate owner of this Polyline. If non-null, the delegate owner replaces the Polyline as the
* pickable object returned during picking. If null, the Polyline itself is the pickable object returned during
* picking.
*
* @param owner the object to use as the pickable object returned during picking, or null to return the Polyline.
*/
public void setDelegateOwner(Object owner)
{
this.delegateOwner = owner;
}
/**
* Returns this Polyline's enclosing volume as an {@link gov.nasa.worldwind.geom.Extent} in model coordinates, given
* a specified {@link gov.nasa.worldwind.globes.Globe} and vertical exaggeration (see {@link
* gov.nasa.worldwind.SceneController#getVerticalExaggeration()}.
*
* @param globe the Globe this Polyline is related to.
* @param verticalExaggeration the vertical exaggeration to apply.
*
* @return this Polyline's Extent in model coordinates.
*
* @throws IllegalArgumentException if the Globe is null.
*/
public Extent getExtent(Globe globe, double verticalExaggeration)
{
if (globe == null)
{
String message = Logging.getMessage("nullValue.GlobeIsNull");
Logging.logger().severe(message);
throw new IllegalArgumentException(message);
}
return this.computeExtent(globe, verticalExaggeration);
}
/**
* Returns this Polyline's enclosing volume as an {@link gov.nasa.worldwind.geom.Extent} in model coordinates, given
* a specified {@link gov.nasa.worldwind.render.DrawContext}. The returned Extent may be different than the Extent
* returned by calling {@link #getExtent(gov.nasa.worldwind.globes.Globe, double)} with the DrawContext's Globe and
* vertical exaggeration. Additionally, this may cache the computed extent and is therefore potentially faster than
* calling {@link #getExtent(gov.nasa.worldwind.globes.Globe, double)}.
*
* @param dc the current DrawContext.
*
* @return this Polyline's Extent in model coordinates.
*
* @throws IllegalArgumentException if the DrawContext is null, or if the Globe held by the DrawContext is null.
*/
public Extent getExtent(DrawContext dc)
{
if (dc == null)
{
String message = Logging.getMessage("nullValue.DrawContextIsNull");
Logging.logger().severe(message);
throw new IllegalArgumentException(message);
}
if (dc.getGlobe() == null)
{
String message = Logging.getMessage("nullValue.DrawingContextGlobeIsNull");
Logging.logger().severe(message);
throw new IllegalArgumentException(message);
}
ExtentInfo extentInfo = this.extents.get(dc.getGlobe());
if (extentInfo != null && extentInfo.isValid(dc))
{
return extentInfo.extent;
}
else
{
extentInfo = new ExtentInfo(this.computeExtent(dc), dc);
this.extents.put(dc.getGlobe(), extentInfo);
return extentInfo.extent;
}
}
protected Extent computeExtent(Globe globe, double verticalExaggeration)
{
Sector sector = Sector.boundingSector(this.getPositions());
double[] minAndMaxElevations;
if (this.isFollowTerrain())
{
minAndMaxElevations = globe.getMinAndMaxElevations(sector);
}
else
{
minAndMaxElevations = computeElevationExtremes(this.getPositions());
}
minAndMaxElevations[0] += this.getOffset();
minAndMaxElevations[1] += this.getOffset();
return Sector.computeBoundingBox(globe, verticalExaggeration, sector, minAndMaxElevations[0],
minAndMaxElevations[1]);
}
protected Extent computeExtent(DrawContext dc)
{
return this.computeExtent(dc.getGlobe(), dc.getVerticalExaggeration());
}
protected static double[] computeElevationExtremes(Iterable positions)
{
double[] extremes = new double[] {Double.MAX_VALUE, -Double.MAX_VALUE};
for (Position pos : positions)
{
if (extremes[0] > pos.getElevation())
extremes[0] = pos.getElevation(); // min
if (extremes[1] < pos.getElevation())
extremes[1] = pos.getElevation(); // max
}
return extremes;
}
public double getDistanceFromEye()
{
return this.eyeDistance;
}
public void pick(DrawContext dc, Point pickPoint)
{
// This method is called only when ordered renderables are being drawn.
// Arg checked within call to render.
this.pickSupport.clearPickList();
try
{
this.pickSupport.beginPicking(dc);
this.render(dc);
}
finally
{
this.pickSupport.endPicking(dc);
this.pickSupport.resolvePick(dc, pickPoint, this.pickLayer);
}
}
public void render(DrawContext dc)
{
// This render method is called three times during frame generation. It's first called as a {@link Renderable}
// during Renderable
picking. It's called again during normal rendering. And it's called a third
// time as an OrderedRenderable. The first two calls determine whether to add the polyline to the ordered
// renderable list during pick and render. The third call just draws the ordered renderable.
if (dc == null)
{
String msg = Logging.getMessage("nullValue.DrawContextIsNull");
Logging.logger().severe(msg);
throw new IllegalArgumentException(msg);
}
if (dc.getSurfaceGeometry() == null)
return;
this.draw(dc);
}
/**
* If the scene controller is rendering ordered renderables, this method draws this placemark's image as an ordered
* renderable. Otherwise the method determines whether this instance should be added to the ordered renderable
* list.
*
* The Cartesian and screen points of the placemark are computed during the first call per frame and re-used in
* subsequent calls of that frame.
*
* @param dc the current draw context.
*/
protected void draw(DrawContext dc)
{
if (dc.isOrderedRenderingMode())
{
this.drawOrderedRenderable(dc);
return;
}
// The rest of the code in this method determines whether to queue an ordered renderable for the polyline.
if (this.positions.size() < 2)
return;
// vertices potentially computed every frame to follow terrain changes
if (this.currentSpans == null || (this.followTerrain && this.geomGenTimeStamp != dc.getFrameTimeStamp())
|| this.geomGenVE != dc.getVerticalExaggeration())
{
// Reference center must be computed prior to computing vertices.
this.computeReferenceCenter(dc);
this.eyeDistance = this.referenceCenterPoint.distanceTo3(dc.getView().getEyePoint());
this.makeVertices(dc);
this.geomGenTimeStamp = dc.getFrameTimeStamp();
this.geomGenVE = dc.getVerticalExaggeration();
}
if (this.currentSpans == null || this.currentSpans.size() < 1)
return;
if (this.intersectsFrustum(dc))
{
if (dc.isPickingMode())
this.pickLayer = dc.getCurrentLayer();
dc.addOrderedRenderable(this); // add the ordered renderable
}
}
protected void drawOrderedRenderable(DrawContext dc)
{
GL2 gl = dc.getGL().getGL2(); // GL initialization checks for GL2 compatibility.
int attrBits = GL2.GL_HINT_BIT | GL2.GL_CURRENT_BIT | GL2.GL_LINE_BIT;
if (!dc.isPickingMode())
{
if (this.color.getAlpha() != 255)
attrBits |= GL.GL_COLOR_BUFFER_BIT;
}
gl.glPushAttrib(attrBits);
dc.getView().pushReferenceCenter(dc, this.referenceCenterPoint);
boolean projectionOffsetPushed = false; // keep track for error recovery
try
{
if (!dc.isPickingMode())
{
if (this.color.getAlpha() != 255)
{
gl.glEnable(GL.GL_BLEND);
gl.glBlendFunc(GL.GL_SRC_ALPHA, GL.GL_ONE_MINUS_SRC_ALPHA);
}
gl.glColor4ub((byte) this.color.getRed(), (byte) this.color.getGreen(),
(byte) this.color.getBlue(), (byte) this.color.getAlpha());
}
else
{
// We cannot depend on the layer to set a pick color for us because this Polyline is picked during ordered
// rendering. Therefore we set the pick color ourselves.
Color pickColor = dc.getUniquePickColor();
Object userObject = this.getDelegateOwner() != null ? this.getDelegateOwner() : this;
this.pickSupport.addPickableObject(pickColor.getRGB(), userObject, null);
gl.glColor3ub((byte) pickColor.getRed(), (byte) pickColor.getGreen(), (byte) pickColor.getBlue());
}
if (this.stippleFactor > 0)
{
gl.glEnable(GL2.GL_LINE_STIPPLE);
gl.glLineStipple(this.stippleFactor, this.stipplePattern);
}
else
{
gl.glDisable(GL2.GL_LINE_STIPPLE);
}
int hintAttr = GL2.GL_LINE_SMOOTH_HINT;
if (this.filled)
hintAttr = GL2.GL_POLYGON_SMOOTH_HINT;
gl.glHint(hintAttr, this.antiAliasHint);
int primType = GL2.GL_LINE_STRIP;
if (this.filled)
primType = GL2.GL_POLYGON;
if (dc.isPickingMode())
gl.glLineWidth((float) this.lineWidth + 8);
else
gl.glLineWidth((float) this.lineWidth);
if (this.followTerrain)
{
dc.pushProjectionOffest(0.99);
projectionOffsetPushed = true;
}
if (this.currentSpans == null)
return;
for (List span : this.currentSpans)
{
if (span == null)
continue;
// Since segments can very often be very short -- two vertices -- use explicit rendering. The
// overhead of batched rendering, e.g., gl.glDrawArrays, is too high because it requires copying
// the vertices into a DoubleBuffer, and DoubleBuffer creation and access performs relatively poorly.
gl.glBegin(primType);
for (Vec4 p : span)
{
gl.glVertex3d(p.x, p.y, p.z);
}
gl.glEnd();
}
if (this.highlighted)
{
if (!dc.isPickingMode())
{
if (this.highlightColor.getAlpha() != 255)
{
gl.glEnable(GL.GL_BLEND);
gl.glBlendFunc(GL.GL_SRC_ALPHA, GL.GL_ONE_MINUS_SRC_ALPHA);
}
gl.glColor4ub((byte) this.highlightColor.getRed(), (byte) this.highlightColor.getGreen(),
(byte) this.highlightColor.getBlue(), (byte) this.highlightColor.getAlpha());
gl.glLineWidth((float) this.lineWidth + 2);
for (List span : this.currentSpans)
{
if (span == null)
continue;
gl.glBegin(primType);
for (Vec4 p : span)
{
gl.glVertex3d(p.x, p.y, p.z);
}
gl.glEnd();
}
}
}
}
finally
{
if (projectionOffsetPushed)
dc.popProjectionOffest();
gl.glPopAttrib();
dc.getView().popReferenceCenter(dc);
}
}
/**
* Indicates whether the shape is visible in the current view.
*
* @param dc the draw context.
*
* @return true if the shape is visible, otherwise false.
*/
protected boolean intersectsFrustum(DrawContext dc)
{
Extent extent = this.getExtent(dc);
if (extent == null)
return true; // don't know the visibility, shape hasn't been computed yet
if (dc.isPickingMode())
return dc.getPickFrustums().intersectsAny(extent);
return dc.getView().getFrustumInModelCoordinates().intersects(extent);
}
protected void makeVertices(DrawContext dc)
{
if (this.currentSpans == null)
this.currentSpans = new ArrayList>();
else
this.currentSpans.clear();
if (this.positions.size() < 1)
return;
Position posA = this.positions.get(0);
Vec4 ptA = this.computePoint(dc, posA, true);
for (int i = 1; i <= this.positions.size(); i++)
{
Position posB;
if (i < this.positions.size())
posB = this.positions.get(i);
else if (this.closed)
posB = this.positions.get(0);
else
break;
Vec4 ptB = this.computePoint(dc, posB, true);
if (this.followTerrain && !this.isSegmentVisible(dc, posA, posB, ptA, ptB))
{
posA = posB;
ptA = ptB;
continue;
}
ArrayList span;
span = this.makeSegment(dc, posA, posB, ptA, ptB);
if (span != null)
this.addSpan(span);
posA = posB;
ptA = ptB;
}
}
protected void addSpan(ArrayList span)
{
if (span != null && span.size() > 0)
this.currentSpans.add(span);
}
protected boolean isSegmentVisible(DrawContext dc, Position posA, Position posB, Vec4 ptA, Vec4 ptB)
{
Frustum f = dc.getView().getFrustumInModelCoordinates();
if (f.contains(ptA))
return true;
if (f.contains(ptB))
return true;
if (ptA.equals(ptB))
return false;
Position posC = Position.interpolateRhumb(0.5, posA, posB);
Vec4 ptC = this.computePoint(dc, posC, true);
if (f.contains(ptC))
return true;
double r = Line.distanceToSegment(ptA, ptB, ptC);
Cylinder cyl = new Cylinder(ptA, ptB, r == 0 ? 1 : r);
return cyl.intersects(dc.getView().getFrustumInModelCoordinates());
}
protected Vec4 computePoint(DrawContext dc, Position pos, boolean applyOffset)
{
if (this.followTerrain)
{
double height = !applyOffset ? 0 : this.offset;
// computeTerrainPoint will apply vertical exaggeration
return dc.computeTerrainPoint(pos.getLatitude(), pos.getLongitude(), height);
}
else
{
double height = pos.getElevation() + (applyOffset ? this.offset : 0);
return dc.getGlobe().computePointFromPosition(pos.getLatitude(), pos.getLongitude(),
height * dc.getVerticalExaggeration());
}
}
protected double computeSegmentLength(DrawContext dc, Position posA, Position posB)
{
LatLon llA = new LatLon(posA.getLatitude(), posA.getLongitude());
LatLon llB = new LatLon(posB.getLatitude(), posB.getLongitude());
Angle ang = LatLon.greatCircleDistance(llA, llB);
if (this.followTerrain)
{
return ang.radians * (dc.getGlobe().getRadius() + this.offset * dc.getVerticalExaggeration());
}
else
{
double height = this.offset + 0.5 * (posA.getElevation() + posB.getElevation());
return ang.radians * (dc.getGlobe().getRadius() + height * dc.getVerticalExaggeration());
}
}
protected ArrayList makeSegment(DrawContext dc, Position posA, Position posB, Vec4 ptA, Vec4 ptB)
{
ArrayList span = null;
double arcLength = this.computeSegmentLength(dc, posA, posB);
if (arcLength <= 0) // points differing only in altitude
{
span = this.addPointToSpan(ptA, span);
if (!ptA.equals(ptB))
span = this.addPointToSpan(ptB, span);
return span;
}
// Variables for great circle and rhumb computation.
Angle segmentAzimuth = null;
Angle segmentDistance = null;
for (double s = 0, p = 0; s < 1; )
{
if (this.followTerrain)
p += this.terrainConformance * dc.getView().computePixelSizeAtDistance(
ptA.distanceTo3(dc.getView().getEyePoint()));
else
p += arcLength / this.numSubsegments;
s = p / arcLength;
Position pos;
if (s >= 1)
{
pos = posB;
}
else if (this.pathType == LINEAR)
{
if (segmentAzimuth == null)
{
segmentAzimuth = LatLon.linearAzimuth(posA, posB);
segmentDistance = LatLon.linearDistance(posA, posB);
}
Angle distance = Angle.fromRadians(s * segmentDistance.radians);
LatLon latLon = LatLon.linearEndPosition(posA, segmentAzimuth, distance);
pos = new Position(latLon, (1 - s) * posA.getElevation() + s * posB.getElevation());
}
else if (this.pathType
== RHUMB_LINE) // or LOXODROME (note that loxodrome is translated to RHUMB_LINE in setPathType)
{
if (segmentAzimuth == null)
{
segmentAzimuth = LatLon.rhumbAzimuth(posA, posB);
segmentDistance = LatLon.rhumbDistance(posA, posB);
}
Angle distance = Angle.fromRadians(s * segmentDistance.radians);
LatLon latLon = LatLon.rhumbEndPosition(posA, segmentAzimuth, distance);
pos = new Position(latLon, (1 - s) * posA.getElevation() + s * posB.getElevation());
}
else // GREAT_CIRCLE
{
if (segmentAzimuth == null)
{
segmentAzimuth = LatLon.greatCircleAzimuth(posA, posB);
segmentDistance = LatLon.greatCircleDistance(posA, posB);
}
Angle distance = Angle.fromRadians(s * segmentDistance.radians);
LatLon latLon = LatLon.greatCircleEndPosition(posA, segmentAzimuth, distance);
pos = new Position(latLon, (1 - s) * posA.getElevation() + s * posB.getElevation());
}
ptB = this.computePoint(dc, pos, true);
span = this.clipAndAdd(dc, ptA, ptB, span);
ptA = ptB;
}
return span;
}
@SuppressWarnings({"UnusedDeclaration"})
protected ArrayList clipAndAdd(DrawContext dc, Vec4 ptA, Vec4 ptB, ArrayList span)
{
// Line clipping appears to be useful only for long lines with few segments. It's costly otherwise.
// TODO: Investigate trade-off of line clipping.
// if (Line.clipToFrustum(ptA, ptB, dc.getView().getFrustumInModelCoordinates()) == null)
// {
// if (span != null)
// {
// this.addSpan(span);
// span = null;
// }
// return span;
// }
if (span == null)
span = this.addPointToSpan(ptA, span);
return this.addPointToSpan(ptB, span);
}
protected ArrayList addPointToSpan(Vec4 p, ArrayList span)
{
if (span == null)
span = new ArrayList();
span.add(p.subtract3(this.referenceCenterPoint));
return span;
}
protected void computeReferenceCenter(DrawContext dc)
{
// The reference position is null if this Polyline has no positions. In this case computing the Polyline's
// Cartesian reference point is meaningless because the Polyline has no geographic location. Therefore we exit
// without updating the reference point.
Position refPos = this.getReferencePosition();
if (refPos == null)
return;
this.referenceCenterPoint = dc.computeTerrainPoint(refPos.getLatitude(), refPos.getLongitude(),
this.offset);
}
public Position getReferencePosition()
{
if (this.positions.size() < 1)
{
return null;
}
else if (this.positions.size() < 3)
{
return this.positions.get(0);
}
else
{
return this.positions.get(this.positions.size() / 2);
}
}
public void move(Position delta)
{
if (delta == null)
{
String msg = Logging.getMessage("nullValue.PositionIsNull");
Logging.logger().severe(msg);
throw new IllegalArgumentException(msg);
}
Position refPos = this.getReferencePosition();
// The reference position is null if this Polyline has no positions. In this case moving the Polyline by a
// relative delta is meaningless because the Polyline has no geographic location. Therefore we fail softly by
// exiting and doing nothing.
if (refPos == null)
return;
this.moveTo(refPos.add(delta));
}
public void moveTo(Position position)
{
if (position == null)
{
String msg = Logging.getMessage("nullValue.PositionIsNull");
Logging.logger().severe(msg);
throw new IllegalArgumentException(msg);
}
this.reset();
this.extents.clear();
Position oldRef = this.getReferencePosition();
// The reference position is null if this Polyline has no positions. In this case moving the Polyline to a new
// reference position is meaningless because the Polyline has no geographic location. Therefore we fail softly
// by exiting and doing nothing.
if (oldRef == null)
return;
double elevDelta = position.getElevation() - oldRef.getElevation();
for (int i = 0; i < this.positions.size(); i++)
{
Position pos = this.positions.get(i);
Angle distance = LatLon.greatCircleDistance(oldRef, pos);
Angle azimuth = LatLon.greatCircleAzimuth(oldRef, pos);
LatLon newLocation = LatLon.greatCircleEndPosition(position, azimuth, distance);
double newElev = pos.getElevation() + elevDelta;
this.positions.set(i, new Position(newLocation, newElev));
}
}
/**
* Returns an XML state document String describing the public attributes of this Polyline.
*
* @return XML state document string describing this Polyline.
*/
public String getRestorableState()
{
RestorableSupport rs = RestorableSupport.newRestorableSupport();
// Creating a new RestorableSupport failed. RestorableSupport logged the problem, so just return null.
if (rs == null)
return null;
if (this.color != null)
{
String encodedColor = RestorableSupport.encodeColor(this.color);
if (encodedColor != null)
rs.addStateValueAsString("color", encodedColor);
}
if (this.highlightColor != null)
{
String encodedColor = RestorableSupport.encodeColor(this.highlightColor);
if (encodedColor != null)
rs.addStateValueAsString("highlightColor", encodedColor);
}
if (this.positions != null)
{
// Create the base "positions" state object.
RestorableSupport.StateObject positionsStateObj = rs.addStateObject("positions");
if (positionsStateObj != null)
{
for (Position p : this.positions)
{
// Save each position only if all parts (latitude, longitude, and elevation) can be
// saved. We will not save a partial iconPosition (for example, just the elevation).
if (p != null && p.getLatitude() != null && p.getLongitude() != null)
{
// Create a nested "position" element underneath the base "positions".
RestorableSupport.StateObject pStateObj =
rs.addStateObject(positionsStateObj, "position");
if (pStateObj != null)
{
rs.addStateValueAsDouble(pStateObj, "latitudeDegrees",
p.getLatitude().degrees);
rs.addStateValueAsDouble(pStateObj, "longitudeDegrees",
p.getLongitude().degrees);
rs.addStateValueAsDouble(pStateObj, "elevation",
p.getElevation());
}
}
}
}
}
rs.addStateValueAsInteger("antiAliasHint", this.antiAliasHint);
rs.addStateValueAsBoolean("filled", this.filled);
rs.addStateValueAsBoolean("closed", this.closed);
rs.addStateValueAsBoolean("highlighted", this.highlighted);
rs.addStateValueAsInteger("pathType", this.pathType);
rs.addStateValueAsBoolean("followTerrain", this.followTerrain);
rs.addStateValueAsDouble("offset", this.offset);
rs.addStateValueAsDouble("terrainConformance", this.terrainConformance);
rs.addStateValueAsDouble("lineWidth", this.lineWidth);
rs.addStateValueAsInteger("stipplePattern", this.stipplePattern);
rs.addStateValueAsInteger("stippleFactor", this.stippleFactor);
rs.addStateValueAsInteger("numSubsegments", this.numSubsegments);
RestorableSupport.StateObject so = rs.addStateObject(null, "avlist");
for (Map.Entry avp : this.getEntries())
{
this.getRestorableStateForAVPair(avp.getKey(), avp.getValue() != null ? avp.getValue() : "", rs, so);
}
return rs.getStateAsXml();
}
/**
* Restores publicly settable attribute values found in the specified XML state document String. The document
* specified by stateInXml
must be a well formed XML document String, or this will throw an
* IllegalArgumentException. Unknown structures in stateInXml
are benign, because they will simply be
* ignored.
*
* @param stateInXml an XML document String describing a Polyline.
*
* @throws IllegalArgumentException If stateInXml
is null, or if stateInXml
is not a well
* formed XML document String.
*/
public void restoreState(String stateInXml)
{
if (stateInXml == null)
{
String message = Logging.getMessage("nullValue.StringIsNull");
Logging.logger().severe(message);
throw new IllegalArgumentException(message);
}
RestorableSupport restorableSupport;
try
{
restorableSupport = RestorableSupport.parse(stateInXml);
}
catch (Exception e)
{
// Parsing the document specified by stateInXml failed.
String message = Logging.getMessage("generic.ExceptionAttemptingToParseStateXml", stateInXml);
Logging.logger().severe(message);
throw new IllegalArgumentException(message, e);
}
String colorState = restorableSupport.getStateValueAsString("color");
if (colorState != null)
{
Color color = RestorableSupport.decodeColor(colorState);
if (color != null)
setColor(color);
}
colorState = restorableSupport.getStateValueAsString("highlightColor");
if (colorState != null)
{
Color color = RestorableSupport.decodeColor(colorState);
if (color != null)
setHighlightColor(color);
}
// Get the base "positions" state object.
RestorableSupport.StateObject positionsStateObj = restorableSupport.getStateObject("positions");
if (positionsStateObj != null)
{
ArrayList newPositions = new ArrayList();
// Get the nested "position" states beneath the base "positions".
RestorableSupport.StateObject[] positionStateArray =
restorableSupport.getAllStateObjects(positionsStateObj, "position");
if (positionStateArray != null && positionStateArray.length != 0)
{
for (RestorableSupport.StateObject pStateObj : positionStateArray)
{
if (pStateObj != null)
{
// Restore each position only if all parts are available.
// We will not restore a partial position (for example, just the elevation).
Double latitudeState = restorableSupport.getStateValueAsDouble(pStateObj, "latitudeDegrees");
Double longitudeState = restorableSupport.getStateValueAsDouble(pStateObj, "longitudeDegrees");
Double elevationState = restorableSupport.getStateValueAsDouble(pStateObj, "elevation");
if (latitudeState != null && longitudeState != null && elevationState != null)
newPositions.add(Position.fromDegrees(latitudeState, longitudeState, elevationState));
}
}
}
// Even if there are no actual positions specified, we set positions as an empty list.
// An empty set of positions is still a valid state.
setPositions(newPositions);
}
Integer antiAliasHintState = restorableSupport.getStateValueAsInteger("antiAliasHint");
if (antiAliasHintState != null)
setAntiAliasHint(antiAliasHintState);
Boolean isFilledState = restorableSupport.getStateValueAsBoolean("filled");
if (isFilledState != null)
setFilled(isFilledState);
Boolean isClosedState = restorableSupport.getStateValueAsBoolean("closed");
if (isClosedState != null)
setClosed(isClosedState);
Boolean isHighlightedState = restorableSupport.getStateValueAsBoolean("highlighted");
if (isHighlightedState != null)
setHighlighted(isHighlightedState);
Integer pathTypeState = restorableSupport.getStateValueAsInteger("pathType");
if (pathTypeState != null)
setPathType(pathTypeState);
Boolean isFollowTerrainState = restorableSupport.getStateValueAsBoolean("followTerrain");
if (isFollowTerrainState != null)
setFollowTerrain(isFollowTerrainState);
Double offsetState = restorableSupport.getStateValueAsDouble("offset");
if (offsetState != null)
setOffset(offsetState);
Double terrainConformanceState = restorableSupport.getStateValueAsDouble("terrainConformance");
if (terrainConformanceState != null)
setTerrainConformance(terrainConformanceState);
Double lineWidthState = restorableSupport.getStateValueAsDouble("lineWidth");
if (lineWidthState != null)
setLineWidth(lineWidthState);
Integer stipplePatternState = restorableSupport.getStateValueAsInteger("stipplePattern");
if (stipplePatternState != null)
setStipplePattern(stipplePatternState.shortValue());
Integer stippleFactorState = restorableSupport.getStateValueAsInteger("stippleFactor");
if (stippleFactorState != null)
setStippleFactor(stippleFactorState);
Integer numSubsegmentsState = restorableSupport.getStateValueAsInteger("numSubsegments");
if (numSubsegmentsState != null)
setNumSubsegments(numSubsegmentsState);
RestorableSupport.StateObject so = restorableSupport.getStateObject(null, "avlist");
if (so != null)
{
RestorableSupport.StateObject[] avpairs = restorableSupport.getAllStateObjects(so, "");
if (avpairs != null)
{
for (RestorableSupport.StateObject avp : avpairs)
{
if (avp != null)
this.setValue(avp.getName(), avp.getValue());
}
}
}
}
}