src.gov.nasa.worldwind.symbology.milstd2525.graphics.areas.SectorRangeFan 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.symbology.milstd2525.graphics.areas;
import gov.nasa.worldwind.WorldWind;
import gov.nasa.worldwind.avlist.AVKey;
import gov.nasa.worldwind.geom.*;
import gov.nasa.worldwind.globes.Globe;
import gov.nasa.worldwind.render.*;
import gov.nasa.worldwind.symbology.*;
import gov.nasa.worldwind.symbology.milstd2525.AbstractMilStd2525TacticalGraphic;
import gov.nasa.worldwind.symbology.milstd2525.graphics.TacGrpSidc;
import gov.nasa.worldwind.util.*;
import java.text.*;
import java.util.*;
/**
* Implementation of the Sector Weapon/Sensor Range Fans graphic (2.X.4.3.4.2). The range fans are defined by a center
* position, list of radii, and list of left and right azimuths. If no azimuths are specified, the fans will be drawn as
* full circles.
*
* @author pabercrombie
* @version $Id: SectorRangeFan.java 1055 2013-01-01 17:09:32Z dcollins $
*/
public class SectorRangeFan extends AbstractMilStd2525TacticalGraphic implements PreRenderable
{
/** Default number of intervals used to draw each arcs. */
public final static int DEFAULT_NUM_INTERVALS = 32;
/** Default length of the Center Of Sector line, as a fraction of the final range fan radius. */
public final static double DEFAULT_CENTER_OF_SECTOR_LENGTH = 1.2;
/** Default length of the arrowhead, as a fraction of the Center Of Sector line length. */
public final static double DEFAULT_ARROWHEAD_LENGTH = 0.05;
/** Default angle of the arrowhead. */
public final static Angle DEFAULT_ARROWHEAD_ANGLE = Angle.fromDegrees(60.0);
/**
* Azimuth labels are placed to the side of the line that they label. This value specifies a percentage that is
* multiplied by the maximum radius in the range fan to compute a distance for this offset.
*/
protected final static double AZIMUTH_LABEL_OFFSET = 0.03;
/** Default number format used to create azimuth and radius labels. */
public final static NumberFormat DEFAULT_NUMBER_FORMAT = new DecimalFormat("#");
/** Length of the arrowhead from base to tip, as a fraction of the Center Of Sector line length. */
protected Angle arrowAngle = DEFAULT_ARROWHEAD_ANGLE;
/** Angle of the arrowhead. */
protected double arrowLength = DEFAULT_ARROWHEAD_LENGTH;
/** Length of the Center Of Sector line, as a fraction of the last range fan radius. */
protected double centerOfSectorLength = DEFAULT_CENTER_OF_SECTOR_LENGTH;
/** Number of intervals used to draw each arcs. */
protected int intervals = DEFAULT_NUM_INTERVALS;
/** Number format used to create azimuth labels. */
protected NumberFormat azimuthFormat = DEFAULT_NUMBER_FORMAT;
/** Number format used to create radius labels. */
protected NumberFormat radiusFormat = DEFAULT_NUMBER_FORMAT;
/** Position of the center of the range fan. */
protected Position position;
/** Rings that make up the range fan. */
protected List paths;
/** Polygon to draw a filled arrow head on the Center Of Sector line. */
protected SurfacePolygon arrowHead;
/** Symbol drawn at the center of the range fan. */
protected TacticalSymbol symbol;
/** Attributes applied to the symbol. */
protected TacticalSymbolAttributes symbolAttributes;
/** Radii of the range fans, in meters. */
protected Iterable radii;
/** Azimuths of the range fans. The azimuths are specified in pairs, first the left azimuth, then the right azimuth. */
protected Iterable azimuths;
/** Altitudes of the range fans. */
protected Iterable altitudes;
/** Azimuth of the Center Of Sector arrow. */
protected Angle centerAzimuth;
/** Maximum radius in the range fan. */
protected double maxRadius;
/**
* Indicates the graphics supported by this class.
*
* @return List of masked SIDC strings that identify graphics that this class supports.
*/
public static List getSupportedGraphics()
{
return Arrays.asList(TacGrpSidc.FSUPP_ARS_WPNRF_SCR);
}
/**
* Create the range fan.
*
* @param sidc Symbol code the identifies the graphic.
*/
public SectorRangeFan(String sidc)
{
super(sidc);
}
/**
* Indicates the angle of the arrowhead.
*
* @return Angle of the arrowhead in the graphic.
*/
public Angle getArrowAngle()
{
return this.arrowAngle;
}
/**
* Specifies the angle of the arrowhead in the graphic.
*
* @param arrowAngle The angle of the arrowhead. Must be greater than zero degrees and less than 90 degrees.
*/
public void setArrowAngle(Angle arrowAngle)
{
if (arrowAngle == null)
{
String msg = Logging.getMessage("nullValue.AngleIsNull");
Logging.logger().severe(msg);
throw new IllegalArgumentException(msg);
}
if (arrowAngle.degrees <= 0 || arrowAngle.degrees >= 90)
{
String msg = Logging.getMessage("generic.AngleOutOfRange");
Logging.logger().severe(msg);
throw new IllegalArgumentException(msg);
}
this.arrowAngle = arrowAngle;
}
/**
* Indicates the length of the arrowhead.
*
* @return The length of the arrowhead as a fraction of the total line length.
*/
public double getArrowLength()
{
return this.arrowLength;
}
/**
* Specifies the length of the arrowhead.
*
* @param arrowLength Length of the arrowhead as a fraction of the total line length. If the arrowhead length is
* 0.25, then the arrowhead length will be one quarter of the total line length.
*/
public void setArrowLength(double arrowLength)
{
if (arrowLength < 0)
{
String msg = Logging.getMessage("generic.ArgumentOutOfRange");
Logging.logger().severe(msg);
throw new IllegalArgumentException(msg);
}
this.arrowLength = arrowLength;
}
/**
* Indicates the length of the Center Of Sector line.
*
* @return The length of the Center Of Sector as a fraction of the final range fan radius.
*/
public double getCenterOfSectorLength()
{
return this.centerOfSectorLength;
}
/**
* Specifies the length of the Center Of Sector line.
*
* @param centerOfSectorLength Length of the Center Of Sector arrow as a fraction of the final range fan radius.
*/
public void setCenterOfSector(double centerOfSectorLength)
{
if (centerOfSectorLength < 0)
{
String msg = Logging.getMessage("generic.ArgumentOutOfRange");
Logging.logger().severe(msg);
throw new IllegalArgumentException(msg);
}
this.centerOfSectorLength = centerOfSectorLength;
}
/**
* Indicates the number of intervals used to draw the arc in this graphic.
*
* @return Intervals used to draw arc.
*/
public int getIntervals()
{
return this.intervals;
}
/**
* Specifies the number of intervals used to draw the arc in this graphic. More intervals will result in a smoother
* looking arc.
*
* @param intervals Number of intervals for drawing the arc.
*/
public void setIntervals(int intervals)
{
if (intervals < 1)
{
String message = Logging.getMessage("generic.ArgumentOutOfRange", intervals);
Logging.logger().severe(message);
throw new IllegalArgumentException(message);
}
this.intervals = intervals;
this.reset();
}
/**
* Indicates the number format applied to azimuth values to create the azimuth labels.
*
* @return NumberFormat used to create azimuth labels.
*/
public NumberFormat getAzimuthFormat()
{
return this.azimuthFormat;
}
/**
* Specifies the number format applied to azimuth values to create the azimuth labels.
*
* @param azimuthFormat NumberFormat to create azimuth labels.
*/
public void setAzimuthFormat(NumberFormat azimuthFormat)
{
if (azimuthFormat == null)
{
String message = Logging.getMessage("nullValue.Format");
Logging.logger().severe(message);
throw new IllegalArgumentException(message);
}
this.azimuthFormat = azimuthFormat;
}
/**
* Indicates the number format applied to radius values to create the radius labels.
*
* @return NumberFormat used to create radius labels.
*/
public NumberFormat getRadiusFormat()
{
return this.radiusFormat;
}
/**
* Specifies the number format applied to radius values to create the radius labels.
*
* @param radiusFormat NumberFormat to create radius labels.
*/
public void setRadiusFormat(NumberFormat radiusFormat)
{
if (radiusFormat == null)
{
String message = Logging.getMessage("nullValue.Format");
Logging.logger().severe(message);
throw new IllegalArgumentException(message);
}
this.radiusFormat = radiusFormat;
}
/**
* Indicates the center position of the range ran.
*
* @return The range fan center position.
*/
public Position getPosition()
{
return this.getReferencePosition();
}
/**
* Specifies the center position of the range ran.
*
* @param position The new center position.
*/
public void setPosition(Position position)
{
this.moveTo(position);
}
/**
* {@inheritDoc}
*
* @param positions Control points. This graphic uses only one control point, which determines the center of the
* circle.
*/
public void setPositions(Iterable positions)
{
if (positions == null)
{
String message = Logging.getMessage("nullValue.PositionsListIsNull");
Logging.logger().severe(message);
throw new IllegalArgumentException(message);
}
Iterator iterator = positions.iterator();
if (!iterator.hasNext())
{
String message = Logging.getMessage("generic.InsufficientPositions");
Logging.logger().severe(message);
throw new IllegalArgumentException(message);
}
this.position = iterator.next();
this.reset();
if (this.symbol != null)
this.symbol.setPosition(this.position);
}
/** {@inheritDoc} */
@Override
@SuppressWarnings("unchecked")
public void setModifier(String modifier, Object value)
{
if (SymbologyConstants.DISTANCE.equals(modifier))
{
if (value instanceof Iterable)
{
this.setRadii((Iterable) value);
}
else if (value instanceof Double)
{
this.setRadii(Arrays.asList((Double) value));
}
}
else if (SymbologyConstants.AZIMUTH.equals(modifier))
{
if (value instanceof Iterable)
{
// Store the Iterable in an unnecessary variable to suppress Java 7 compiler warnings on Windows.
Iterable iterable = (Iterable) value;
this.setAzimuths(iterable);
}
else if (value instanceof Angle)
{
this.setAzimuths(Arrays.asList((Angle) value));
}
}
else if (SymbologyConstants.ALTITUDE_DEPTH.equals(modifier))
{
if (value instanceof Iterable)
{
// Store the Iterable in an unnecessary variable to suppress Java 7 compiler warnings on Windows.
Iterable iterable = (Iterable) value;
this.setAltitudes(iterable);
}
else if (value != null)
{
this.setAltitudes(Arrays.asList(value.toString()));
}
}
else if (SymbologyConstants.SYMBOL_INDICATOR.equals(modifier) && value instanceof String)
{
this.setSymbol((String) value);
}
else
{
super.setModifier(modifier, value);
}
}
/** {@inheritDoc} */
@Override
public Object getModifier(String modifier)
{
if (SymbologyConstants.DISTANCE.equals(modifier))
return this.getRadii();
else if (SymbologyConstants.AZIMUTH.equals(modifier))
return this.getAzimuths();
else if (SymbologyConstants.ALTITUDE_DEPTH.equals(modifier))
return this.getAltitudes();
else if (SymbologyConstants.SYMBOL_INDICATOR.equals(modifier))
return this.getSymbol();
else
return super.getModifier(modifier);
}
/**
* Indicates the radii of the rings that make up the range fan.
*
* @return List of radii, in meters. This method never returns null. If there are no rings this returns an empty
* list.
*/
public Iterable getRadii()
{
if (this.radii != null)
return this.radii;
return Collections.emptyList();
}
/**
* Specifies the radii of the rings that make up the range fan.
*
* @param radii List of radii, in meters. A circle will be created for each radius.
*/
public void setRadii(Iterable radii)
{
this.radii = radii;
this.onModifierChanged();
this.reset();
}
/**
* Indicates the left and right azimuths of the fans in this graphic. The list contains pairs of azimuths, first
* left and then right.
*
* @return Left and right azimuths, measured clockwise from North. This method never returns null. If there are no
* azimuths, returns an empty list.
*/
public Iterable getAzimuths()
{
if (this.azimuths != null)
return this.azimuths;
return Collections.emptyList();
}
/**
* Specifies the left and right azimuths of the range fans in this graphic. The provided iterable must specify pairs
* of azimuths: first left azimuth, first right azimuth, second left, second right, etc.
*
* @param azimuths Left and right azimuths, measured clockwise from North.
*/
public void setAzimuths(Iterable azimuths)
{
if (azimuths == null)
{
String message = Logging.getMessage("nullValue.ListIsNull");
Logging.logger().severe(message);
throw new IllegalArgumentException(message);
}
this.azimuths = azimuths;
this.onModifierChanged();
this.reset();
}
/**
* Indicates the altitudes of the rings that make up the range fan. Note that the range fan is always drawn on the
* surface. The altitude strings are displayed as text.
*
* @return List of altitude strings. This method never returns null. If there are no altitudes, returns an empty
* list.
*/
public Iterable getAltitudes()
{
if (this.altitudes != null)
return this.altitudes;
return Collections.emptyList();
}
/**
* Specifies the altitudes of the rings that make up the range fan. Note that the range fan is always drawn on the
* surface. The altitude strings are displayed as text.
*
* @param altitudes List of altitude strings.
*/
public void setAltitudes(Iterable altitudes)
{
if (altitudes == null)
{
String message = Logging.getMessage("nullValue.ListIsNull");
Logging.logger().severe(message);
throw new IllegalArgumentException(message);
}
this.altitudes = altitudes;
}
/**
* Indicates a symbol drawn at the center of the range fan.
*
* @return The identifier of a symbol drawn at the center of the range fan. May be null.
*/
public String getSymbol()
{
return this.symbol != null ? this.symbol.getIdentifier() : null;
}
/**
* Specifies a symbol to draw at the center of the range fan. Equivalent to setting the {@link
* SymbologyConstants#SYMBOL_INDICATOR} modifier. The symbol's position will be changed to match the range fan
* center position.
*
* @param sidc The identifier of a MIL-STD-2525C tactical symbol to draw at the center of the range fan. May be null
* to indicate that no symbol is drawn.
*/
public void setSymbol(String sidc)
{
if (sidc != null)
{
if (this.symbolAttributes == null)
this.symbolAttributes = new BasicTacticalSymbolAttributes();
this.symbol = this.createSymbol(sidc, this.getPosition(), this.symbolAttributes);
}
else
{
// Null value indicates no symbol.
this.symbol = null;
this.symbolAttributes = null;
}
this.onModifierChanged();
}
/** {@inheritDoc} */
public Iterable getPositions()
{
return Arrays.asList(this.position);
}
/** {@inheritDoc} */
public Position getReferencePosition()
{
return this.position;
}
/** {@inheritDoc} */
public void preRender(DrawContext dc)
{
if (!this.isVisible())
{
return;
}
this.determineActiveAttributes();
if (this.paths == null)
{
this.createShapes(dc);
}
if (this.arrowHead != null)
{
this.arrowHead.preRender(dc);
}
}
/**
* Render the polygon.
*
* @param dc Current draw context.
*/
protected void doRenderGraphic(DrawContext dc)
{
for (Path path : this.paths)
{
path.render(dc);
}
if (this.arrowHead != null)
{
this.arrowHead.render(dc);
}
}
/** {@inheritDoc} Overridden to render symbol at the center of the range fan. */
@Override
protected void doRenderGraphicModifiers(DrawContext dc)
{
super.doRenderGraphicModifiers(dc);
if (this.symbol != null)
{
this.symbol.render(dc);
}
}
/** {@inheritDoc} */
protected void applyDelegateOwner(Object owner)
{
if (this.paths != null)
{
for (Path path : this.paths)
{
path.setDelegateOwner(owner);
}
}
if (this.arrowHead != null)
{
this.arrowHead.setDelegateOwner(owner);
}
}
/** Regenerate the graphics positions on the next frame. */
protected void reset()
{
this.paths = null;
}
/**
* Create the paths required to draw the graphic.
*
* @param dc Current draw context.
*/
protected void createShapes(DrawContext dc)
{
this.paths = new ArrayList();
Iterator radii = this.getRadii().iterator();
Iterator azimuths = this.getAzimuths().iterator();
Angle prevLeftAzimuth = Angle.NEG180;
Angle prevRightAzimuth = Angle.POS180;
double prevRadius = 0;
// Create range fan arcs.
while (radii.hasNext())
{
double radius = radii.next();
if (radius > this.maxRadius)
this.maxRadius = radius;
Angle leftAzimuth = azimuths.hasNext() ? azimuths.next() : prevLeftAzimuth;
Angle rightAzimuth = azimuths.hasNext() ? azimuths.next() : prevRightAzimuth;
leftAzimuth = this.normalizeAzimuth(leftAzimuth);
rightAzimuth = this.normalizeAzimuth(rightAzimuth);
List positions = new ArrayList();
// Create an arc to complete the left side of the previous fan, if this fan is larger. If this fan is smaller
// this will add a single point to the position list at the range of the previous radius.
this.createArc(dc, prevRadius, Angle.max(leftAzimuth, prevLeftAzimuth), leftAzimuth, positions);
// Create the arc for this fan.
this.createArc(dc, radius, leftAzimuth, rightAzimuth, positions);
// Create an arc to complete the right side of the previous fan.
this.createArc(dc, prevRadius, rightAzimuth, Angle.min(rightAzimuth, prevRightAzimuth), positions);
this.paths.add(this.createPath(positions));
prevRadius = radius;
prevLeftAzimuth = leftAzimuth;
prevRightAzimuth = rightAzimuth;
}
// Create the Center of Sector arrow if the fan is less than a full circle. If the fan is a full circle then it
// doesn't make sense to draw an arbitrary arrow.
boolean fullCircle = Math.abs(prevLeftAzimuth.subtract(prevRightAzimuth).degrees) >= 360;
if (!fullCircle)
{
this.centerAzimuth = this.computeCenterSectorAngle(prevLeftAzimuth, prevRightAzimuth);
this.createCenterOfSectorArrow(dc, centerAzimuth, prevRadius);
}
else
{
this.centerAzimuth = Angle.POS180; // Due South
}
}
/**
* Create shapes to draw the Center Of Sector arrow. This arrow bisects the final range fan.
*
* @param dc Current draw context.
* @param centerAzimuth Azimuth of the Center Of Sector arrow.
* @param finalRadius Radius, in meters, of the final range fan.
*/
protected void createCenterOfSectorArrow(DrawContext dc, Angle centerAzimuth, double finalRadius)
{
Position center = this.getPosition();
// Create the line par of the arrow.
List positions = new ArrayList();
positions.add(center);
this.createArc(dc, finalRadius * this.getCenterOfSectorLength(), centerAzimuth, centerAzimuth,
positions);
this.paths.add(this.createPath(positions));
// The final position added by createArc above is the tip of the Center Of Sector arrow head.
Position arrowTip = positions.get(positions.size() - 1);
// Create a polygon to draw the arrow head.
this.arrowHead = this.createPolygon();
positions = this.computeArrowheadPositions(dc, center, arrowTip, this.getArrowLength(),
this.getArrowAngle());
this.arrowHead.setLocations(positions);
}
/**
* Compute the angle of the Center Of Sector line. The center sector angle is computed as the average of the left
* and right azimuths of the last range fan in the graphic. MIL-STD-2525C does not specify how this angle should be
* computed, but the graphic template (pg. 693) suggests that the Center Of Sector line should bisect the last range
* fan arc.
*
* @param finalLeftAzimuth Left azimuth of the last range fan in the graphic.
* @param finalRightAzimuth Right azimuth of the last range fan in the graphic.
*
* @return Azimuth, from North, of the Center Of Sector line.
*/
protected Angle computeCenterSectorAngle(Angle finalLeftAzimuth, Angle finalRightAzimuth)
{
return finalLeftAzimuth.add(finalRightAzimuth).divide(2.0);
}
/**
* Create positions to draw an arc around the graphic's center position. The arc is described by a radius, left
* azimuth, and right azimuth. The arc positions are added onto an existing list of positions, in left to right
* order (the arc draws from the left azimuth to the right azimuth).
*
* If the left and right azimuths are equal, then this methods adds a single position to the list at the desired
* azimuth and radius.
*
* @param dc Current draw context.
* @param radius Radius of the circular segment, in meters.
* @param leftAzimuth Azimuth (from North) of the left side of the arc.
* @param rightAzimuth Azimuth (from North) of teh right side of the arc.
* @param positions List to collect positions for the arc.
*/
protected void createArc(DrawContext dc, double radius, Angle leftAzimuth, Angle rightAzimuth,
List positions)
{
Globe globe = dc.getGlobe();
int intervals = this.getIntervals();
Position center = this.getPosition();
double globeRadius = globe.getRadiusAt(center.getLatitude(), center.getLongitude());
double radiusRadians = radius / globeRadius;
// If the left and right azimuths are equal then just add a single point and return.
if (leftAzimuth.equals(rightAzimuth))
{
LatLon ll = LatLon.greatCircleEndPosition(center, leftAzimuth.radians, radiusRadians);
positions.add(new Position(ll, 0));
return;
}
Angle arcAngle = rightAzimuth.subtract(leftAzimuth);
Angle da = arcAngle.divide(intervals);
for (int i = 0; i < intervals + 1; i++)
{
double angle = i * da.radians + leftAzimuth.radians;
LatLon ll = LatLon.greatCircleEndPosition(center, angle, radiusRadians);
positions.add(new Position(ll, 0));
}
}
/**
* Compute the positions of the arrow head for the sector center line.
*
* @param dc Current draw context
* @param base Position of the arrow's starting point.
* @param tip Position of the arrow head tip.
* @param arrowLength Length of the arrowhead as a fraction of the total line length.
* @param arrowAngle Angle of the arrow head.
*
* @return Positions required to draw the arrow head.
*/
protected List computeArrowheadPositions(DrawContext dc, Position base, Position tip, double arrowLength,
Angle arrowAngle)
{
// Build a triangle to represent the arrowhead. The triangle is built from two vectors, one parallel to the
// segment, and one perpendicular to it.
Globe globe = dc.getGlobe();
Vec4 ptA = globe.computePointFromPosition(base);
Vec4 ptB = globe.computePointFromPosition(tip);
// Compute parallel component
Vec4 parallel = ptA.subtract3(ptB);
Vec4 surfaceNormal = globe.computeSurfaceNormalAtPoint(ptB);
// Compute perpendicular component
Vec4 perpendicular = surfaceNormal.cross3(parallel);
double finalArrowLength = arrowLength * parallel.getLength3();
double arrowHalfWidth = finalArrowLength * arrowAngle.tanHalfAngle();
perpendicular = perpendicular.normalize3().multiply3(arrowHalfWidth);
parallel = parallel.normalize3().multiply3(finalArrowLength);
// Compute geometry of direction arrow
Vec4 vertex1 = ptB.add3(parallel).add3(perpendicular);
Vec4 vertex2 = ptB.add3(parallel).subtract3(perpendicular);
return TacticalGraphicUtil.asPositionList(globe, vertex1, vertex2, ptB);
}
/** Create labels for the start and end of the path. */
@Override
protected void createLabels()
{
Iterable radii = this.getRadii();
if (radii == null)
return;
Iterator altitudes = this.getAltitudes().iterator();
Iterator azimuths = this.getAzimuths().iterator();
Angle leftAzimuth = null;
Angle rightAzimuth = null;
for (Double radius : radii)
{
if (azimuths.hasNext())
leftAzimuth = azimuths.next();
if (azimuths.hasNext())
rightAzimuth = azimuths.next();
String alt = null;
if (altitudes.hasNext())
alt = altitudes.next();
this.addLabel(this.createRangeLabelString(radius, alt));
if (leftAzimuth != null)
this.addLabel(this.createAzimuthLabelString(leftAzimuth));
if (rightAzimuth != null)
this.addLabel(this.createAzimuthLabelString(rightAzimuth));
}
}
/** Create azimuth labels. */
protected void createAzimuthLabels()
{
Iterable azimuths = this.getAzimuths();
if (azimuths == null)
return;
for (Angle azimuth : azimuths)
{
this.addLabel(this.createAzimuthLabelString(azimuth));
}
}
/**
* Create text for a range label.
*
* @param radius Range of the ring, in meters.
* @param altitude Altitude string.
*
* @return Range label text for this ring.
*/
protected String createRangeLabelString(double radius, String altitude)
{
NumberFormat df = this.getRadiusFormat();
StringBuilder sb = new StringBuilder();
sb.append("RG ").append(df.format(radius));
if (!WWUtil.isEmpty(altitude))
{
sb.append("\nALT ").append(altitude);
}
return sb.toString();
}
/**
* Create text for an azimuth label.
*
* @param azimuth Azimuth, measured clockwise from North.
*
* @return Azimuth label text.
*/
protected String createAzimuthLabelString(Angle azimuth)
{
NumberFormat df = this.getAzimuthFormat();
return df.format(azimuth.degrees);
}
/** {@inheritDoc} */
@Override
protected void determineLabelPositions(DrawContext dc)
{
if (this.labels == null)
return;
Position center = this.getPosition();
Iterator labelIterator = this.labels.iterator();
Iterator radii = this.getRadii().iterator();
Iterator azimuths = this.getAzimuths().iterator();
Angle leftAzimuth = null;
Angle rightAzimuth = null;
double prevRadius = 0;
double globeRadius = dc.getGlobe().getRadiusAt(center);
while (radii.hasNext() && labelIterator.hasNext())
{
if (azimuths.hasNext())
leftAzimuth = azimuths.next();
if (azimuths.hasNext())
rightAzimuth = azimuths.next();
leftAzimuth = this.normalizeAzimuth(leftAzimuth);
rightAzimuth = this.normalizeAzimuth(rightAzimuth);
// The labels come in sets of three: radius, left azimuth, right azimuth
TacticalGraphicLabel rangeLabel = labelIterator.next();
TacticalGraphicLabel leftLabel = null; // Left azimuth label
TacticalGraphicLabel rightLabel = null; // Right azimuth label
if (leftAzimuth != null && labelIterator.hasNext())
leftLabel = labelIterator.next();
if (rightAzimuth != null && labelIterator.hasNext())
rightLabel = labelIterator.next();
double radius = radii.next();
double avgRadius = (radius + prevRadius) / 2.0;
double radiusRadians = avgRadius / globeRadius;
// Find the range label position
Position position = this.determineRangeLabelPosition(center, this.centerAzimuth, leftAzimuth, rightAzimuth,
radiusRadians);
rangeLabel.setPosition(position);
// Compute an angular offset that will put the label a little bit to the side of range fan line.
double offset = this.computeAzimuthLabelOffset(avgRadius, this.maxRadius);
// Find left azimuth label position
if (leftAzimuth != null && leftLabel != null)
{
LatLon ll = LatLon.greatCircleEndPosition(center, leftAzimuth.radians - offset, radiusRadians);
leftLabel.setPosition(new Position(ll, 0));
}
// Find right azimuth label position
if (rightAzimuth != null && rightLabel != null)
{
LatLon ll = LatLon.greatCircleEndPosition(center, rightAzimuth.radians + offset, radiusRadians);
rightLabel.setPosition(new Position(ll, 0));
}
prevRadius = radius;
}
}
/**
* Compute an angular offset to apply to a azimuth label. This angle will be added to the azimuth of the label's
* azimuth in order to place the label a little bit to the side of the line that it applies to.
*
* @param radius Radius at which the label will be placed.
* @param maxRadius Maximum radius in the range fan.
*
* @return Angle, in radians, to add to the range fan azimuth in order to determine the label position.
*/
protected double computeAzimuthLabelOffset(double radius, double maxRadius)
{
return Math.asin(AZIMUTH_LABEL_OFFSET * maxRadius / radius);
}
/**
* Determine the position of a range label for a ring in the range fan. The method finds a point to either the left
* or right of the center line, depending on which has more space for the label.
*
* @param center Center of the range fan.
* @param centerAzimuth Azimuth of the Center Of Sector arrow.
* @param leftAzimuth Left azimuth of this ring.
* @param rightAzimuth Right azimuth of this ring.
* @param radiusRadians Radius, in radians, at which to place the label.
*
* @return Position for the range label on this ring.
*/
protected Position determineRangeLabelPosition(Position center, Angle centerAzimuth, Angle leftAzimuth,
Angle rightAzimuth, double radiusRadians)
{
// If either left or right azimuth is not specified, use the center instead.
leftAzimuth = (leftAzimuth != null) ? leftAzimuth : centerAzimuth;
rightAzimuth = (rightAzimuth != null) ? rightAzimuth : centerAzimuth;
// Determine the angular distance between the Center Of Sector line and the left and right sides of the fan.
double deltaLeft = Math.abs(centerAzimuth.subtract(leftAzimuth).degrees);
double deltaRight = Math.abs(centerAzimuth.subtract(rightAzimuth).degrees);
// Place the range label in the larger wedge.
Angle labelAzimuth = (deltaLeft > deltaRight) ? leftAzimuth : rightAzimuth;
// Place the label midway between the Center Of Sector arrow and the side of the fan.
labelAzimuth = labelAzimuth.add(centerAzimuth).divide(2.0);
LatLon ll = LatLon.greatCircleEndPosition(center, labelAzimuth.radians, radiusRadians);
return new Position(ll, 0);
}
/**
* Normalize an azimuth angle to the range [-180:180] degrees.
*
* @param azimuth Azimuth to normalize.
*
* @return Normalized azimuth. Returns null if {@code azimuth} is null.
*/
protected Angle normalizeAzimuth(Angle azimuth)
{
// The azimuth is not actually a longitude, but the normalization formula is the same as for longitude.
if (azimuth != null)
return Angle.normalizedLongitude(azimuth);
return null;
}
/** {@inheritDoc} Overridden to turn on shape interiors. */
@Override
protected void applyDefaultAttributes(ShapeAttributes attributes)
{
super.applyDefaultAttributes(attributes);
// Turn on the shape interior for the arrow head. All other parts of the graphic are Paths, which do not draw
// an interior, so this setting only affects the arrow head.
Material material = this.getDefaultMaterial();
attributes.setInteriorMaterial(material);
attributes.setDrawInterior(true);
}
/** {@inheritDoc} Overridden to update symbol attributes. */
@Override
protected void determineActiveAttributes()
{
super.determineActiveAttributes();
// Apply active attributes to the symbol.
if (this.symbolAttributes != null)
{
ShapeAttributes activeAttributes = this.getActiveShapeAttributes();
this.symbolAttributes.setOpacity(activeAttributes.getInteriorOpacity());
this.symbolAttributes.setScale(this.activeOverrides.getScale());
}
}
/**
* Create and configure the Path used to render this graphic.
*
* @param positions Positions that define the path.
*
* @return New path configured with defaults appropriate for this type of graphic.
*/
protected Path createPath(List positions)
{
Path path = new Path(positions);
path.setFollowTerrain(true);
path.setPathType(AVKey.GREAT_CIRCLE);
path.setAltitudeMode(WorldWind.CLAMP_TO_GROUND);
path.setDelegateOwner(this.getActiveDelegateOwner());
path.setAttributes(this.getActiveShapeAttributes());
return path;
}
/**
* Create and configure a SurfacePolygon to render the arrow head on the sector center line.
*
* @return New surface polygon.
*/
protected SurfacePolygon createPolygon()
{
SurfacePolygon polygon = new SurfacePolygon();
polygon.setDelegateOwner(this.getActiveDelegateOwner());
polygon.setAttributes(this.getActiveShapeAttributes());
return polygon;
}
}