src.gov.nasa.worldwind.render.PointPlacemark 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 com.jogamp.opengl.util.awt.TextRenderer;
import gov.nasa.worldwind.*;
import gov.nasa.worldwind.avlist.AVKey;
import gov.nasa.worldwind.geom.*;
import gov.nasa.worldwind.layers.Layer;
import gov.nasa.worldwind.ogc.kml.KMLConstants;
import gov.nasa.worldwind.ogc.kml.impl.KMLExportUtil;
import gov.nasa.worldwind.pick.*;
import gov.nasa.worldwind.util.*;
import javax.media.opengl.*;
import javax.xml.stream.*;
import java.awt.*;
import java.awt.geom.*;
import java.io.*;
import java.net.URL;
import java.util.*;
import static gov.nasa.worldwind.ogc.kml.impl.KMLExportUtil.kmlBoolean;
/**
* Represents a point placemark consisting of an image, an optional line linking the image to a corresponding point on
* the terrain, and an optional label. The image and the label are displayed in the plane of the screen.
*
* Point placemarks have separate attributes for normal rendering and highlighted rendering. If highlighting is
* requested but no highlight attributes are specified, the normal attributes are used. If the normal attributes are not
* specified, default attributes are used. See {@link #getDefaultAttributes()}.
*
* This class implements and extends the functionality of a KML Point.
*
* Point placemarks can participate in global text decluttering by setting their decluttering-enabled flag to {@code
* true}. See {@link #setEnableDecluttering(boolean)}. The default for this flag is {@code false}. When participating in
* decluttering, only the point placemark's label is considered when determining interference with other text.
*
* @author tag
* @version $Id: PointPlacemark.java 1942 2014-04-16 02:21:09Z tgaskins $
*/
public class PointPlacemark extends WWObjectImpl
implements OrderedRenderable, Locatable, Movable, Highlightable, Exportable, Declutterable
{
/** The scale to use when highlighting if no highlight attributes are specified. */
protected static final Double DEFAULT_HIGHLIGHT_SCALE = 1.3;
/** The label offset to use if none is specified but an image has been specified. */
protected static final Offset DEFAULT_LABEL_OFFSET_IF_UNSPECIFIED = new Offset(1d, 0.6d, AVKey.FRACTION,
AVKey.FRACTION);
/** The point size to use when none is specified. */
protected static final Double DEFAULT_POINT_SIZE = 5d;
/** The attributes used if attributes are not specified. */
protected static final PointPlacemarkAttributes defaultAttributes = new PointPlacemarkAttributes();
static
{
defaultAttributes.setImageAddress(PointPlacemarkAttributes.DEFAULT_IMAGE_PATH);
defaultAttributes.setImageOffset(PointPlacemarkAttributes.DEFAULT_IMAGE_OFFSET);
defaultAttributes.setLabelOffset(PointPlacemarkAttributes.DEFAULT_LABEL_OFFSET);
defaultAttributes.setScale(PointPlacemarkAttributes.DEFAULT_IMAGE_SCALE);
defaultAttributes.setLabelScale(PointPlacemarkAttributes.DEFAULT_LABEL_SCALE);
}
protected Position position;
protected String labelText;
protected PointPlacemarkAttributes normalAttrs;
protected PointPlacemarkAttributes highlightAttrs;
protected PointPlacemarkAttributes activeAttributes = new PointPlacemarkAttributes(); // re-determined each frame
protected Map textures = new HashMap(); // holds the textures created
protected WWTexture activeTexture; // determined each frame
protected boolean highlighted;
protected boolean visible = true;
protected int altitudeMode = WorldWind.CLAMP_TO_GROUND;
protected boolean lineEnabled;
protected boolean applyVerticalExaggeration = true;
protected int linePickWidth = 10;
protected boolean enableBatchRendering = true;
protected boolean enableBatchPicking = true;
protected Object delegateOwner;
protected boolean clipToHorizon = true;
protected boolean enableDecluttering = false;
// Values computed once per frame and reused during the frame as needed.
protected long frameNumber = -1; // identifies frame used to calculate these values
protected Vec4 placePoint; // the Cartesian point corresponding to the placemark position
protected Vec4 terrainPoint; // point on the terrain extruded from the placemark position.
protected Vec4 screenPoint; // the projection of the place-point in the viewport (on the screen)
protected double eyeDistance; // used to order the placemark as an ordered renderable
protected double dx; // offsets needed to position image relative to the placemark position
protected double dy;
protected Layer pickLayer; // shape's layer when ordered renderable was created
protected Rectangle2D labelBounds; // cached label bounds
protected Font boundsFont; // the font associated with the cached label bounds
protected PickSupport pickSupport = new PickSupport();
/**
* Construct a point placemark.
*
* @param position the placemark position.
*
* @throws IllegalArgumentException if the position is null.
*/
public PointPlacemark(Position position)
{
if (position == null)
{
String message = Logging.getMessage("nullValue.PositionIsNull");
Logging.logger().severe(message);
throw new IllegalArgumentException(message);
}
this.position = position;
}
/**
* Sets the placemark's position.
*
* @param position the placemark position.
*
* @throws IllegalArgumentException if the position is null.
*/
public void setPosition(Position position)
{
if (position == null)
{
String message = Logging.getMessage("nullValue.PositionIsNull");
Logging.logger().severe(message);
throw new IllegalArgumentException(message);
}
this.position = position;
}
/**
* Returns the placemark's position.
*
* @return the placemark's position.
*/
public Position getPosition()
{
return this.position;
}
/**
* Indicates whether the placemark is drawn when in view.
*
* @return true if the placemark is drawn when in view, otherwise false.
*/
public boolean isVisible()
{
return this.visible;
}
/**
* Specifies whether the placemark is drawn when in view.
*
* @param visible true if the placemark is drawn when in view, otherwise false.
*/
public void setVisible(boolean visible)
{
this.visible = visible;
}
/**
* Returns the placemark's altitude mode. See {@link #setAltitudeMode(int)} for a description of the modes.
*
* @return the placemark's altitude mode.
*/
public int getAltitudeMode()
{
return this.altitudeMode;
}
/**
* Specifies the placemark's altitude mode. Recognized modes are: - @link WorldWind#CLAMP_TO_GROUND}
* -- the point is placed on the terrain at the latitude and longitude of its position.
- @link
* WorldWind#RELATIVE_TO_GROUND} -- the point is placed above the terrain at the latitude and longitude of its
* position and the distance specified by its elevation.
- {@link WorldWind#ABSOLUTE} -- the point is
* placed at its specified position.
*
* @param altitudeMode the altitude mode
*/
public void setAltitudeMode(int altitudeMode)
{
this.altitudeMode = altitudeMode;
}
/** {@inheritDoc} * */
public double getDistanceFromEye()
{
return this.eyeDistance;
}
/**
* Indicates whether a line from the placemark point to the corresponding position on the terrain is drawn.
*
* @return true if the line is drawn, otherwise false.
*/
public boolean isLineEnabled()
{
return lineEnabled;
}
/**
* Specifies whether a line from the placemark point to the corresponding position on the terrain is drawn.
*
* @param lineEnabled true if the line is drawn, otherwise false.
*/
public void setLineEnabled(boolean lineEnabled)
{
this.lineEnabled = lineEnabled;
}
/**
* Specifies the attributes used when the placemark is drawn normally, not highlighted.
*
* @param attrs the attributes to use in normal mode. May be null to indicate use of default attributes.
*/
public void setAttributes(PointPlacemarkAttributes attrs)
{
if (this.normalAttrs != null && this.normalAttrs.getImageAddress() != null)
this.textures.remove(this.normalAttrs.getImageAddress());
this.normalAttrs = attrs;
}
/**
* Returns the attributes used when the placemark is drawn normally, not highlighted.
*
* @return the attributes used in normal mode. May be null to indicate use of default attributes.
*/
public PointPlacemarkAttributes getAttributes()
{
return this.normalAttrs;
}
/**
* Specifies the attributes used to draw the placemark when it's highlighted.
*
* @param attrs the attributes to use in normal mode. May be null to indicate use of the normal attributes.
*/
public void setHighlightAttributes(PointPlacemarkAttributes attrs)
{
if (this.highlightAttrs != null && this.highlightAttrs.getImageAddress() != null)
this.textures.remove(this.highlightAttrs.getImageAddress());
this.highlightAttrs = attrs;
}
/**
* Returns the attributes used to draw the placemark when it's highlighted.
*
* @return the attributes used in normal mode. May be null to indicate use of the normal attributes.
*/
public PointPlacemarkAttributes getHighlightAttributes()
{
return this.highlightAttrs;
}
/**
* Returns the attributes used if normal attributes are not specified.
*
* @return the default attributes.
*/
public PointPlacemarkAttributes getDefaultAttributes()
{
return defaultAttributes;
}
/**
* Indicates whether the placemark is drawn highlighted.
*
* @return true if the placemark is drawn highlighted, otherwise false.
*/
public boolean isHighlighted()
{
return this.highlighted;
}
/**
* Specfies whether the placemark is drawn highlighted.
*
* @param highlighted true if the placemark is drawn highlighted, otherwise false.
*/
public void setHighlighted(boolean highlighted)
{
this.highlighted = highlighted;
}
/**
* Returns the placemark's label text.
*
* @return the placemark's label next, which man be null.
*/
public String getLabelText()
{
return labelText;
}
/**
* Specifies the placemark's label text that is displayed alongside the placemark.
*
* @param labelText the placemark label text. If null, no label is displayed.
*/
public void setLabelText(String labelText)
{
this.labelText = labelText != null ? labelText.trim() : null;
}
public boolean isApplyVerticalExaggeration()
{
return applyVerticalExaggeration;
}
public void setApplyVerticalExaggeration(boolean applyVerticalExaggeration)
{
this.applyVerticalExaggeration = applyVerticalExaggeration;
}
public int getLinePickWidth()
{
return linePickWidth;
}
public void setLinePickWidth(int linePickWidth)
{
this.linePickWidth = linePickWidth;
}
/**
* Indicates whether batch rendering is enabled.
*
* @return true if batch rendering is enabled, otherwise false.
*
* @see #setEnableBatchRendering(boolean).
*/
public boolean isEnableBatchRendering()
{
return enableBatchRendering;
}
/**
* Specifies whether adjacent PointPlacemarks in the ordered renderable list may be rendered together if they are
* contained in the same layer. This increases performance and there is seldom a reason to disable it.
*
* @param enableBatchRendering true to enable batch rendering, otherwise false.
*/
public void setEnableBatchRendering(boolean enableBatchRendering)
{
this.enableBatchRendering = enableBatchRendering;
}
/**
* Indicates whether batch picking is enabled.
*
* @return true if batch rendering is enabled, otherwise false.
*
* @see #setEnableBatchPicking(boolean).
*/
public boolean isEnableBatchPicking()
{
return enableBatchPicking;
}
/**
* Returns the delegate owner of this placemark. If non-null, the returned object replaces the placemark as the
* pickable object returned during picking. If null, the placemark itself is the pickable object returned during
* picking.
*
* @return the object used as the pickable object returned during picking, or null to indicate the the placemark is
* returned during picking.
*/
public Object getDelegateOwner()
{
return this.delegateOwner;
}
/**
* Specifies the delegate owner of this placemark. If non-null, the delegate owner replaces the placemark as the
* pickable object returned during picking. If null, the placemark 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 placemark.
*/
public void setDelegateOwner(Object owner)
{
this.delegateOwner = owner;
}
protected PointPlacemarkAttributes getActiveAttributes()
{
return this.activeAttributes;
}
/**
* Specifies whether adjacent PointPlacemarks in the ordered renderable list may be pick-tested together if they are
* contained in the same layer. This increases performance but allows only the top-most of the placemarks to be
* reported in a {@link gov.nasa.worldwind.event.SelectEvent} even if several of the placemarks are at the pick
* position.
*
* Batch rendering ({@link #setEnableBatchRendering(boolean)}) must be enabled in order for batch picking to occur.
*
* @param enableBatchPicking true to enable batch rendering, otherwise false.
*/
public void setEnableBatchPicking(boolean enableBatchPicking)
{
this.enableBatchPicking = enableBatchPicking;
}
/**
* Indicates whether this placemark is shown if it is beyond the horizon.
*
* @return the value of the clip-to-horizon flag. {@code true} if horizon clipping is enabled, otherwise {@code
* false}. The default value is {@code true}.
*/
public boolean isClipToHorizon()
{
return clipToHorizon;
}
/**
* Specifies whether this placemark is shown if it is beyond the horizon.
*
* @param clipToHorizon {@code true} if this placemark should not be shown when beyond the horizon, otherwise {@code
* false}.
*/
public void setClipToHorizon(boolean clipToHorizon)
{
this.clipToHorizon = clipToHorizon;
}
/**
* Indicates whether this placemark participates in global text decluttering.
*
* @return {@code true} if this placemark participates in global text decluttering, otherwise false. The default
* value is {@code false}. Only the placemark's label is considered during decluttering.
*/
public boolean isEnableDecluttering()
{
return enableDecluttering;
}
/**
* Specifies whether this placemark participates in globe text decluttering.
*
* @param enableDecluttering {@code true} if the placemark participates in global text decluttering, otherwise
* {@code false}. The default value is {@code false}. Only the placemark lable is
* considered during decluttering.
*/
public void setEnableDecluttering(boolean enableDecluttering)
{
this.enableDecluttering = enableDecluttering;
}
/**
* Indicates whether a point should be drawn when the active texture is null.
*
* @param dc the current draw context.
*
* @return true if a point should be drawn, otherwise false.
*/
@SuppressWarnings({"UnusedParameters"})
protected boolean isDrawPoint(DrawContext dc)
{
return this.activeTexture == null && this.getActiveAttributes().isUsePointAsDefaultImage();
}
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 placemark and its optional
// line 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;
if (!this.isVisible())
return;
if (dc.isOrderedRenderingMode())
this.drawOrderedRenderable(dc);
else
this.makeOrderedRenderable(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 makeOrderedRenderable(DrawContext dc)
{
// The rest of the code in this method determines whether to queue an ordered renderable for the placemark
// and its optional line.
// Re-use values already calculated this frame.
if (dc.getFrameTimeStamp() != this.frameNumber)
{
this.computePlacemarkPoints(dc);
if (this.placePoint == null || this.screenPoint == null)
return;
this.determineActiveAttributes();
if (this.activeTexture == null && !this.getActiveAttributes().isUsePointAsDefaultImage())
return;
this.computeImageOffset(dc); // calculates offsets to align the image with the hotspot
this.frameNumber = dc.getFrameTimeStamp();
}
if (this.isClipToHorizon())
{
// Don't draw if beyond the horizon.
double horizon = dc.getView().getHorizonDistance();
if (this.eyeDistance > horizon)
return;
}
if (this.intersectsFrustum(dc) || this.isDrawLine(dc))
dc.addOrderedRenderable(this); // add the image ordered renderable
if (dc.isPickingMode())
this.pickLayer = dc.getCurrentLayer();
}
/**
* Determines whether the placemark image intersects the view frustum.
*
* @param dc the current draw context.
*
* @return true if the image intersects the frustum, otherwise false.
*/
protected boolean intersectsFrustum(DrawContext dc)
{
View view = dc.getView();
// Test the placemark's model coordinate point against the near and far clipping planes.
if (this.placePoint != null
&& (view.getFrustumInModelCoordinates().getNear().distanceTo(this.placePoint) < 0
|| view.getFrustumInModelCoordinates().getFar().distanceTo(this.placePoint) < 0))
{
return false;
}
Rectangle rect = this.computeImageRectangle(dc);
if (dc.isPickingMode())
{
// Test image rect against pick frustums. Note that we do this even when the label is visible and the image
// is not because we do not support picking via the label.
return dc.getPickFrustums().intersectsAny(rect);
}
else if (rect.width > 0)
{
return view.getViewport().intersects(rect);
}
else if (mustDrawLabel())
{
// We are drawing a label but not an image. Determine if the placemark point is visible. This case comes up
// when the image scale is zero and the label scale is non-zero.
return view.getViewport().contains((int) this.screenPoint.x, (int) this.screenPoint.y);
}
return false;
}
/**
* Establish the OpenGL state needed to draw Paths.
*
* @param dc the current draw context.
*/
protected void beginDrawing(DrawContext dc)
{
GL2 gl = dc.getGL().getGL2(); // GL initialization checks for GL2 compatibility.
int attrMask =
GL2.GL_DEPTH_BUFFER_BIT // for depth test, depth mask and depth func
| GL2.GL_TRANSFORM_BIT // for modelview and perspective
| GL2.GL_VIEWPORT_BIT // for depth range
| GL2.GL_CURRENT_BIT // for current color
| GL2.GL_COLOR_BUFFER_BIT // for alpha test func and ref, and blend
| GL2.GL_DEPTH_BUFFER_BIT // for depth func
| GL2.GL_ENABLE_BIT // for enable/disable changes
| GL2.GL_HINT_BIT | GL2.GL_LINE_BIT; // for antialiasing and line attrs
gl.glPushAttrib(attrMask);
if (!dc.isPickingMode())
{
gl.glEnable(GL.GL_BLEND);
OGLUtil.applyBlending(gl, false);
}
}
/**
* Pop the state set in beginDrawing.
*
* @param dc the current draw context.
*/
protected void endDrawing(DrawContext dc)
{
GL2 gl = dc.getGL().getGL2(); // GL initialization checks for GL2 compatibility.
gl.glBindTexture(GL.GL_TEXTURE_2D, 0);
gl.glPopAttrib();
}
/**
* Draws the path as an ordered renderable.
*
* @param dc the current draw context.
*/
protected void drawOrderedRenderable(DrawContext dc)
{
this.beginDrawing(dc);
try
{
this.doDrawOrderedRenderable(dc, this.pickSupport);
if (this.isEnableBatchRendering())
this.drawBatched(dc);
}
finally
{
this.endDrawing(dc);
}
}
/**
* Draws this ordered renderable and all subsequent PointPlacemark ordered renderables in the ordered renderable
* list.
*
* @param dc the current draw context.
*/
protected void drawBatched(DrawContext dc)
{
// Draw as many as we can in a batch to save ogl state switching.
Object nextItem = dc.peekOrderedRenderables();
if (!dc.isPickingMode())
{
while (nextItem != null && nextItem instanceof PointPlacemark)
{
PointPlacemark p = (PointPlacemark) nextItem;
if (!p.isEnableBatchRendering())
break;
dc.pollOrderedRenderables(); // take it off the queue
p.doDrawOrderedRenderable(dc, this.pickSupport);
nextItem = dc.peekOrderedRenderables();
}
}
else if (this.isEnableBatchPicking())
{
while (nextItem != null && nextItem instanceof PointPlacemark)
{
PointPlacemark p = (PointPlacemark) nextItem;
if (!p.isEnableBatchRendering() || !p.isEnableBatchPicking())
break;
if (p.pickLayer != this.pickLayer) // batch pick only within a single layer
break;
dc.pollOrderedRenderables(); // take it off the queue
p.doDrawOrderedRenderable(dc, this.pickSupport);
nextItem = dc.peekOrderedRenderables();
}
}
}
/**
* Draw this placemark as an ordered renderable. If in picking mode, add it to the picked object list of specified
* {@link PickSupport}. The PickSupport
may not be the one associated with this instance. During batch
* picking the PickSupport
of the instance initiating the batch picking is used so that all shapes
* rendered in batch are added to the same pick list.
*
* @param dc the current draw context.
* @param pickCandidates a pick support holding the picked object list to add this shape to.
*/
protected void doDrawOrderedRenderable(DrawContext dc, PickSupport pickCandidates)
{
if (this.isDrawLine(dc))
this.drawLine(dc, pickCandidates);
if (this.activeTexture == null)
{
if (this.isDrawPoint(dc))
this.drawPoint(dc, pickCandidates);
return;
}
GL2 gl = dc.getGL().getGL2(); // GL initialization checks for GL2 compatibility.
OGLStackHandler osh = new OGLStackHandler();
try
{
if (dc.isPickingMode())
{
// Set up to replace the non-transparent texture colors with the single pick color.
gl.glEnable(GL.GL_TEXTURE_2D);
gl.glTexEnvf(GL2.GL_TEXTURE_ENV, GL2.GL_TEXTURE_ENV_MODE, GL2.GL_COMBINE);
gl.glTexEnvf(GL2.GL_TEXTURE_ENV, GL2.GL_SRC0_RGB, GL2.GL_PREVIOUS);
gl.glTexEnvf(GL2.GL_TEXTURE_ENV, GL2.GL_COMBINE_RGB, GL2.GL_REPLACE);
Color pickColor = dc.getUniquePickColor();
pickCandidates.addPickableObject(this.createPickedObject(dc, pickColor));
gl.glColor3ub((byte) pickColor.getRed(), (byte) pickColor.getGreen(), (byte) pickColor.getBlue());
}
else
{
gl.glEnable(GL.GL_TEXTURE_2D);
Color color = this.getActiveAttributes().getImageColor();
if (color == null)
color = PointPlacemarkAttributes.DEFAULT_IMAGE_COLOR;
gl.glColor4ub((byte) color.getRed(), (byte) color.getGreen(), (byte) color.getBlue(),
(byte) color.getAlpha());
}
// The image is drawn using a parallel projection.
osh.pushProjectionIdentity(gl);
gl.glOrtho(0d, dc.getView().getViewport().width, 0d, dc.getView().getViewport().height, -1d, 1d);
// Apply the depth buffer but don't change it (for screen-space shapes).
if ((!dc.isDeepPickingEnabled()))
gl.glEnable(GL.GL_DEPTH_TEST);
gl.glDepthMask(false);
// Suppress any fully transparent image pixels.
gl.glEnable(GL2.GL_ALPHA_TEST);
gl.glAlphaFunc(GL2.GL_GREATER, 0.001f);
// Adjust depth of image to bring it slightly forward
double depth = screenPoint.z - (8d * 0.00048875809d);
depth = depth < 0d ? 0d : (depth > 1d ? 1d : depth);
gl.glDepthFunc(GL.GL_LESS);
gl.glDepthRange(depth, depth);
// The image is drawn using a translated and scaled unit quad.
// Translate to screen point and adjust to align hot spot.
osh.pushModelviewIdentity(gl);
gl.glTranslated(screenPoint.x + this.dx, screenPoint.y + this.dy, 0);
// Compute the scale
double xscale;
Double scale = this.getActiveAttributes().getScale();
if (scale != null)
xscale = scale * this.activeTexture.getWidth(dc);
else
xscale = this.activeTexture.getWidth(dc);
double yscale;
if (scale != null)
yscale = scale * this.activeTexture.getHeight(dc);
else
yscale = this.activeTexture.getHeight(dc);
Double heading = getActiveAttributes().getHeading();
Double pitch = getActiveAttributes().getPitch();
// Adjust heading to be relative to globe or screen
if (heading != null)
{
if (AVKey.RELATIVE_TO_GLOBE.equals(this.getActiveAttributes().getHeadingReference()))
heading = dc.getView().getHeading().degrees - heading;
else
heading = -heading;
}
// Apply the heading and pitch if specified.
if (heading != null || pitch != null)
{
gl.glTranslated(xscale / 2, yscale / 2, 0);
if (pitch != null)
gl.glRotated(pitch, 1, 0, 0);
if (heading != null)
gl.glRotated(heading, 0, 0, 1);
gl.glTranslated(-xscale / 2, -yscale / 2, 0);
}
// Scale the unit quad
gl.glScaled(xscale, yscale, 1);
if (this.activeTexture.bind(dc))
dc.drawUnitQuad(activeTexture.getTexCoords());
gl.glDepthRange(0, 1); // reset depth range to the OGL default
//
// gl.glDisable(GL.GL_TEXTURE_2D);
// dc.drawUnitQuadOutline(); // for debugging label placement
if (this.mustDrawLabel() && !dc.isPickingMode()) // don't pick via the label
this.drawLabel(dc);
}
finally
{
if (dc.isPickingMode())
{
gl.glTexEnvf(GL2.GL_TEXTURE_ENV, GL2.GL_TEXTURE_ENV_MODE, OGLUtil.DEFAULT_TEX_ENV_MODE);
gl.glTexEnvf(GL2.GL_TEXTURE_ENV, GL2.GL_SRC0_RGB, OGLUtil.DEFAULT_SRC0_RGB);
gl.glTexEnvf(GL2.GL_TEXTURE_ENV, GL2.GL_COMBINE_RGB, OGLUtil.DEFAULT_COMBINE_RGB);
}
gl.glDisable(GL.GL_TEXTURE_2D);
osh.pop(gl);
}
}
/**
* Create a {@link PickedObject} for this placemark. The PickedObject returned by this method will be added to the
* pick list to represent the current placemark.
*
* @param dc Active draw context.
* @param pickColor Unique color for this PickedObject.
*
* @return A new picked object.
*/
protected PickedObject createPickedObject(DrawContext dc, Color pickColor)
{
Object delegateOwner = this.getDelegateOwner();
return new PickedObject(pickColor.getRGB(), delegateOwner != null ? delegateOwner : this);
}
/**
* Determines if the placemark label will be rendered.
*
* @return True if the label must be drawn.
*/
protected boolean mustDrawLabel()
{
return this.labelText != null;
}
@Override
public Rectangle2D getBounds(DrawContext dc)
{
return this.getLabelBounds(dc);
}
/**
* Determines the screen coordinate boundaries of this placemark's label.
*
* @param dc the current draw context.
*
* @return the label bounds, in lower-left origin screen coordinates.
*/
protected Rectangle2D getLabelBounds(DrawContext dc)
{
if (this.labelText == null)
return null;
double x = (float) (this.screenPoint.x + this.dx);
double y = (float) (this.screenPoint.y + this.dy);
Double imageScale = this.getActiveAttributes().getScale();
Offset os = this.getActiveAttributes().getLabelOffset();
if (os == null)
os = DEFAULT_LABEL_OFFSET_IF_UNSPECIFIED;
double w = this.activeTexture != null ? this.activeTexture.getWidth(dc) : 1;
double h = this.activeTexture != null ? this.activeTexture.getHeight(dc) : 1;
Point.Double offset = os.computeOffset(w, h, imageScale, imageScale);
x += offset.x;
y += offset.y;
Font font = this.getActiveAttributes().getLabelFont();
if (font == null)
font = PointPlacemarkAttributes.DEFAULT_LABEL_FONT;
Rectangle2D bounds;
if (this.labelBounds != null && font == this.boundsFont)
{
bounds = new Rectangle.Double(x, y, this.labelBounds.getWidth(), this.labelBounds.getHeight());
}
else
{
TextRenderer textRenderer = OGLTextRenderer.getOrCreateTextRenderer(dc.getTextRendererCache(), font);
bounds = textRenderer.getBounds(this.labelText);
this.boundsFont = font;
this.labelBounds = bounds;
}
Double labelScale = this.getActiveAttributes().getLabelScale();
if (labelScale != null)
{
double tw = labelScale * bounds.getWidth();
double th = labelScale * bounds.getHeight();
bounds = new Rectangle2D.Double(x, y, tw, th);
}
else
{
bounds = new Rectangle2D.Double(x, y, bounds.getWidth(), bounds.getHeight());
}
return bounds;
}
/**
* Draws the placemark's label if a label is specified.
*
* @param dc the current draw context.
*/
protected void drawLabel(DrawContext dc)
{
if (this.labelText == null)
return;
Color color = this.getActiveAttributes().getLabelColor();
// Use the default color if the active attributes do not specify one.
if (color == null)
color = PointPlacemarkAttributes.DEFAULT_LABEL_COLOR;
// If the label color's alpha component is 0 or less, then the label is completely transparent. Exit
// immediately; the label does not need to be rendered.
if (color.getAlpha() <= 0)
return;
// Apply the label color's alpha component to the background color. This causes both the label foreground and
// background to blend evenly into the frame. If the alpha component is 255 we just use the pre-defined constant
// for BLACK to avoid creating a new background color every frame.
Color backgroundColor = (color.getAlpha() < 255 ? new Color(0, 0, 0, color.getAlpha()) : Color.BLACK);
Font font = this.getActiveAttributes().getLabelFont();
if (font == null)
font = PointPlacemarkAttributes.DEFAULT_LABEL_FONT;
float x = (float) (this.screenPoint.x + this.dx);
float y = (float) (this.screenPoint.y + this.dy);
Double imageScale = this.getActiveAttributes().getScale();
Offset os = this.getActiveAttributes().getLabelOffset();
if (os == null)
os = DEFAULT_LABEL_OFFSET_IF_UNSPECIFIED;
double w = this.activeTexture != null ? this.activeTexture.getWidth(dc) : 1;
double h = this.activeTexture != null ? this.activeTexture.getHeight(dc) : 1;
Point.Double offset = os.computeOffset(w, h, imageScale, imageScale);
x += offset.x;
y += offset.y;
GL2 gl = dc.getGL().getGL2(); // GL initialization checks for GL2 compatibility.
gl.glMatrixMode(GL2.GL_MODELVIEW);
gl.glLoadIdentity();
Double labelScale = this.getActiveAttributes().getLabelScale();
if (labelScale != null)
{
gl.glTranslatef(x, y, 0); // Assumes matrix mode is MODELVIEW
gl.glScaled(labelScale, labelScale, 1);
gl.glTranslatef(-x, -y, 0);
}
// Do not depth buffer the label. (Placemarks beyond the horizon are culled above.)
gl.glDisable(GL.GL_DEPTH_TEST);
gl.glDepthMask(false);
TextRenderer textRenderer = OGLTextRenderer.getOrCreateTextRenderer(dc.getTextRendererCache(), font);
try
{
textRenderer.begin3DRendering();
textRenderer.setColor(backgroundColor);
textRenderer.draw3D(this.labelText, x + 1, y - 1, 0, 1);
textRenderer.setColor(color);
textRenderer.draw3D(this.labelText, x, y, 0, 1);
}
finally
{
textRenderer.end3DRendering();
}
}
/**
* Draws the placemark's line.
*
* @param dc the current draw context.
* @param pickCandidates the pick support object to use when adding this as a pick candidate.
*/
protected void drawLine(DrawContext dc, PickSupport pickCandidates)
{
GL2 gl = dc.getGL().getGL2(); // GL initialization checks for GL2 compatibility.
if ((!dc.isDeepPickingEnabled()))
gl.glEnable(GL.GL_DEPTH_TEST);
gl.glDepthFunc(GL.GL_LEQUAL);
gl.glDepthMask(true);
try
{
dc.getView().pushReferenceCenter(dc, this.placePoint); // draw relative to the place point
this.setLineWidth(dc);
this.setLineColor(dc, pickCandidates);
gl.glBegin(GL2.GL_LINE_STRIP);
gl.glVertex3d(Vec4.ZERO.x, Vec4.ZERO.y, Vec4.ZERO.z);
gl.glVertex3d(this.terrainPoint.x - this.placePoint.x, this.terrainPoint.y - this.placePoint.y,
this.terrainPoint.z - this.placePoint.z);
gl.glEnd();
}
finally
{
dc.getView().popReferenceCenter(dc);
}
}
/**
* Draws the placemark's line.
*
* @param dc the current draw context.
* @param pickCandidates the pick support object to use when adding this as a pick candidate.
*/
protected void drawPoint(DrawContext dc, PickSupport pickCandidates)
{
GL2 gl = dc.getGL().getGL2(); // GL initialization checks for GL2 compatibility.
OGLStackHandler osh = new OGLStackHandler();
try
{
osh.pushAttrib(gl, GL2.GL_POINT_BIT);
this.setLineColor(dc, pickCandidates);
this.setPointSize(dc);
// The point is drawn using a parallel projection.
osh.pushProjectionIdentity(gl);
gl.glOrtho(0d, dc.getView().getViewport().width, 0d, dc.getView().getViewport().height, -1d, 1d);
osh.pushModelviewIdentity(gl);
// Apply the depth buffer but don't change it (for screen-space shapes).
if ((!dc.isDeepPickingEnabled()))
gl.glEnable(GL.GL_DEPTH_TEST);
gl.glDepthMask(false);
// Suppress any fully transparent pixels.
gl.glEnable(GL2.GL_ALPHA_TEST);
gl.glAlphaFunc(GL2.GL_GREATER, 0.001f);
// Adjust depth of point to bring it slightly forward
double depth = this.screenPoint.z - (8d * 0.00048875809d);
depth = depth < 0d ? 0d : (depth > 1d ? 1d : depth);
gl.glDepthFunc(GL.GL_LESS);
gl.glDepthRange(depth, depth);
gl.glBegin(GL2.GL_POINTS);
gl.glVertex3d(this.screenPoint.x, this.screenPoint.y, 0);
gl.glEnd();
gl.glDepthRange(0, 1); // reset depth range to the OGL default
if (!dc.isPickingMode()) // don't pick via the label
this.drawLabel(dc);
}
finally
{
osh.pop(gl);
}
}
/**
* Determines whether the placemark's optional line should be drawn and whether it intersects the view frustum.
*
* @param dc the current draw context.
*
* @return true if the line should be drawn and it intersects the view frustum, otherwise false.
*/
protected boolean isDrawLine(DrawContext dc)
{
if (!this.isLineEnabled() || this.getAltitudeMode() == WorldWind.CLAMP_TO_GROUND || this.terrainPoint == null)
return false;
if (dc.isPickingMode())
return dc.getPickFrustums().intersectsAny(this.placePoint, this.terrainPoint);
else
return dc.getView().getFrustumInModelCoordinates().intersectsSegment(this.placePoint, this.terrainPoint);
}
/**
* Sets the width of the placemark's line during rendering.
*
* @param dc the current draw context.
*/
protected void setLineWidth(DrawContext dc)
{
Double lineWidth = this.getActiveAttributes().getLineWidth();
if (lineWidth != null)
{
GL gl = dc.getGL();
if (dc.isPickingMode())
{
gl.glLineWidth(lineWidth.floatValue() + this.getLinePickWidth());
}
else
gl.glLineWidth(lineWidth.floatValue());
if (!dc.isPickingMode())
{
gl.glHint(GL.GL_LINE_SMOOTH_HINT, this.getActiveAttributes().getAntiAliasHint());
gl.glEnable(GL.GL_LINE_SMOOTH);
}
}
}
/**
* Sets the width of the placemark's point during rendering.
*
* @param dc the current draw context.
*/
protected void setPointSize(DrawContext dc)
{
GL2 gl = dc.getGL().getGL2(); // GL initialization checks for GL2 compatibility.
Double scale = this.getActiveAttributes().getScale();
if (scale == null)
scale = DEFAULT_POINT_SIZE;
if (dc.isPickingMode())
gl.glPointSize(scale.floatValue() + this.getLinePickWidth());
else
gl.glPointSize(scale.floatValue());
if (!dc.isPickingMode())
{
gl.glEnable(GL2.GL_POINT_SMOOTH);
gl.glHint(GL2.GL_POINT_SMOOTH_HINT, GL2.GL_NICEST);
}
}
/**
* Sets the color of the placemark's line during rendering.
*
* @param dc the current draw context.
* @param pickCandidates the pick support object to use when adding this as a pick candidate.
*/
protected void setLineColor(DrawContext dc, PickSupport pickCandidates)
{
GL2 gl = dc.getGL().getGL2(); // GL initialization checks for GL2 compatibility.
if (!dc.isPickingMode())
{
Color color = this.getActiveAttributes().getLineColor();
if (color == null)
color = PointPlacemarkAttributes.DEFAULT_LINE_COLOR;
gl.glColor4ub((byte) color.getRed(), (byte) color.getGreen(), (byte) color.getBlue(),
(byte) color.getAlpha());
}
else
{
Color pickColor = dc.getUniquePickColor();
pickCandidates.addPickableObject(pickColor.getRGB(), this, this.getPosition());
gl.glColor3ub((byte) pickColor.getRed(), (byte) pickColor.getGreen(), (byte) pickColor.getBlue());
}
}
/** Determines which attributes -- normal, highlight or default -- to use each frame. */
protected void determineActiveAttributes()
{
PointPlacemarkAttributes actAttrs = this.getActiveAttributes();
if (this.isHighlighted())
{
if (this.getHighlightAttributes() != null)
{
actAttrs.copy(this.getHighlightAttributes());
// Even though there are highlight attributes, there may not be an image for them, so use the normal image.
if (WWUtil.isEmpty(actAttrs.getImageAddress())
&& this.getAttributes() != null && !WWUtil.isEmpty(this.getAttributes().getImageAddress()))
{
actAttrs.setImageAddress(this.getAttributes().getImageAddress());
if (this.getAttributes().getScale() != null)
actAttrs.setScale(DEFAULT_HIGHLIGHT_SCALE * this.getAttributes().getScale());
else
actAttrs.setScale(DEFAULT_HIGHLIGHT_SCALE);
}
}
else
{
// If no highlight attributes have been specified we need to use the normal attributes but adjust them
// for highlighting.
if (this.getAttributes() != null)
{
actAttrs.copy(this.getAttributes());
if (getAttributes().getScale() != null)
actAttrs.setScale(DEFAULT_HIGHLIGHT_SCALE * this.getAttributes().getScale());
else
actAttrs.setScale(DEFAULT_HIGHLIGHT_SCALE);
}
else
{
actAttrs.copy(defaultAttributes);
if (defaultAttributes.getScale() != null)
actAttrs.setScale(DEFAULT_HIGHLIGHT_SCALE * defaultAttributes.getScale());
else
actAttrs.setScale(DEFAULT_HIGHLIGHT_SCALE);
}
}
}
else if (this.getAttributes() != null)
{
actAttrs.copy(this.getAttributes());
}
else
{
actAttrs.copy(defaultAttributes);
if (this.activeTexture == null && actAttrs.isUsePointAsDefaultImage())
{
actAttrs.setImageAddress(null);
actAttrs.setScale(DEFAULT_POINT_SIZE);
}
}
this.activeTexture = this.chooseTexture(actAttrs);
if (this.activeTexture == null && actAttrs.isUsePointAsDefaultImage())
{
actAttrs.setImageAddress(null);
actAttrs.setImageOffset(null);
if (actAttrs.getScale() == null)
actAttrs.setScale(DEFAULT_POINT_SIZE);
}
}
/**
* Determines the appropriate texture for the current availability.
*
* @param attrs the attributes specifying the placemark image and properties.
*
* @return the appropriate texture, or null if an image is not available.
*/
protected WWTexture chooseTexture(PointPlacemarkAttributes attrs)
{
if (!WWUtil.isEmpty(attrs.getImageAddress()))
{
WWTexture texture = this.textures.get(attrs.getImageAddress());
if (texture != null)
return texture;
texture = this.initializeTexture(attrs.getImageAddress());
if (texture != null)
{
this.textures.put(attrs.getImageAddress(), texture);
return texture;
}
}
if (this.getActiveAttributes().usePointAsDefaultImage)
return null;
// Use the default image if no other is defined or it's not yet available.
WWTexture texture = this.textures.get(defaultAttributes.getImageAddress());
this.getActiveAttributes().setImageOffset(defaultAttributes.getImageOffset());
if (attrs.getScale() != null)
this.getActiveAttributes().setScale(defaultAttributes.getScale() * attrs.getScale());
else
this.getActiveAttributes().setScale(defaultAttributes.getScale());
if (texture == null)
{
URL localUrl = WorldWind.getDataFileStore().requestFile(defaultAttributes.getImageAddress());
if (localUrl != null)
{
texture = new BasicWWTexture(localUrl, true);
this.textures.put(defaultAttributes.getImageAddress(), texture);
}
}
return texture;
}
/**
* Load a texture. If the texture source is not available locally, this method requests the texture source and
* returns null.
*
* @param address Path or URL to the image to load into a texture.
*
* @return The new texture, or null if the texture could not be created because the resource is not yet available
* locally.
*/
protected WWTexture initializeTexture(String address)
{
URL localUrl = WorldWind.getDataFileStore().requestFile(address);
// WWJ-434 (http://issues.worldwind.arc.nasa.gov/jira/browse/WWJ-434) includes a stack trace showing HTTP
// access from BasicWWTexture on the EDT. This should not occur and I (tag) can find no path through the
// code that would cause it. Nevertheless, I've added guard code here to prevent anything other than a file
// based URL to be passed to the BasicWWTexture constructor.
if (localUrl != null)
{
if (!"file".equals(localUrl.getProtocol()))
{
Logging.logger().warning(Logging.getMessage("generic.URLProtocolNotFile", localUrl));
return null;
}
return new BasicWWTexture(localUrl, true);
}
return null;
}
/**
* Computes and stores the placemark's Cartesian location, the Cartesian location of the corresponding point on the
* terrain (if the altitude mode requires it), and the screen-space projection of the placemark's point. Applies the
* placemark's altitude mode when computing the points.
*
* @param dc the current draw context.
*/
protected void computePlacemarkPoints(DrawContext dc)
{
this.placePoint = null;
this.terrainPoint = null;
this.screenPoint = null;
Position pos = this.getPosition();
if (pos == null)
return;
if (this.altitudeMode == WorldWind.CLAMP_TO_GROUND)
{
this.placePoint = dc.computeTerrainPoint(pos.getLatitude(), pos.getLongitude(), 0);
}
else if (this.altitudeMode == WorldWind.RELATIVE_TO_GROUND)
{
this.placePoint = dc.computeTerrainPoint(pos.getLatitude(), pos.getLongitude(), pos.getAltitude());
}
else // ABSOLUTE
{
double height = pos.getElevation()
* (this.isApplyVerticalExaggeration() ? dc.getVerticalExaggeration() : 1);
this.placePoint = dc.getGlobe().computePointFromPosition(pos.getLatitude(), pos.getLongitude(), height);
}
if (this.placePoint == null)
return;
// Compute a terrain point if needed.
if (this.isLineEnabled() && this.altitudeMode != WorldWind.CLAMP_TO_GROUND)
this.terrainPoint = dc.computeTerrainPoint(pos.getLatitude(), pos.getLongitude(), 0);
// Compute the placemark point's screen location.
this.screenPoint = dc.getView().project(this.placePoint);
this.eyeDistance = this.placePoint.distanceTo3(dc.getView().getEyePoint());
}
/**
* Computes the screen-space rectangle bounding the placemark image.
*
* @param dc the current draw context.
*
* @return the bounding rectangle.
*/
protected Rectangle computeImageRectangle(DrawContext dc)
{
double s = this.getActiveAttributes().getScale() != null ? this.getActiveAttributes().getScale() : 1;
double width = s * (this.activeTexture != null ? this.activeTexture.getWidth(dc) : 1);
double height = s * (this.activeTexture != null ? this.activeTexture.getHeight(dc) : 1);
double x = this.screenPoint.x + (this.isDrawPoint(dc) ? -0.5 * s : this.dx);
double y = this.screenPoint.y + (this.isDrawPoint(dc) ? -0.5 * s : this.dy);
return new Rectangle((int) x, (int) y, (int) Math.ceil(width), (int) Math.ceil(height));
}
protected void computeImageOffset(DrawContext dc)
{
// Determine the screen-space offset needed to align the image hot spot with the placemark point.
this.dx = 0;
this.dy = 0;
if (this.isDrawPoint(dc))
return;
Offset os = this.getActiveAttributes().getImageOffset();
if (os == null)
return;
double w = this.activeTexture != null ? this.activeTexture.getWidth(dc) : 1;
double h = this.activeTexture != null ? this.activeTexture.getHeight(dc) : 1;
Point.Double offset = os.computeOffset(w, h,
this.getActiveAttributes().getScale(), this.getActiveAttributes().getScale());
this.dx = -offset.x;
this.dy = -offset.y;
}
/** {@inheritDoc} */
public String isExportFormatSupported(String format)
{
if (KMLConstants.KML_MIME_TYPE.equalsIgnoreCase(format))
return Exportable.FORMAT_SUPPORTED;
else
return Exportable.FORMAT_NOT_SUPPORTED;
}
/** {@inheritDoc} */
public Position getReferencePosition()
{
return this.getPosition();
}
/** {@inheritDoc} */
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 shape has positions. With PointPlacemark, this should never happen
// because its position must always be non-null. We check and this case anyway to handle a subclass overriding
// getReferencePosition and returning null. In this case moving the shape by a relative delta is meaningless
// because the shape has no geographic location. Therefore we fail softly by exiting and doing nothing.
if (refPos == null)
return;
this.moveTo(refPos.add(delta));
}
/** {@inheritDoc} */
public void moveTo(Position position)
{
if (position == null)
{
String msg = Logging.getMessage("nullValue.PositionIsNull");
Logging.logger().severe(msg);
throw new IllegalArgumentException(msg);
}
this.setPosition(position);
}
/**
* Export the Placemark. The {@code output} object will receive the exported data. The type of this object depends
* on the export format. The formats and object types supported by this class are:
*
*
* Format Supported output object types
* ================================================================================
* KML (application/vnd.google-earth.kml+xml) java.io.Writer
* java.io.OutputStream
* javax.xml.stream.XMLStreamWriter
*
*
* @param mimeType MIME type of desired export format.
* @param output An object that will receive the exported data. The type of this object depends on the export
* format (see above).
*
* @throws IOException If an exception occurs writing to the output object.
*/
public void export(String mimeType, Object output) throws IOException
{
if (mimeType == null)
{
String message = Logging.getMessage("nullValue.Format");
Logging.logger().severe(message);
throw new IllegalArgumentException(message);
}
if (output == null)
{
String message = Logging.getMessage("nullValue.OutputBufferIsNull");
Logging.logger().severe(message);
throw new IllegalArgumentException(message);
}
if (KMLConstants.KML_MIME_TYPE.equalsIgnoreCase(mimeType))
{
try
{
exportAsKML(output);
}
catch (XMLStreamException e)
{
Logging.logger().throwing(getClass().getName(), "export", e);
throw new IOException(e);
}
}
else
{
String message = Logging.getMessage("Export.UnsupportedFormat", mimeType);
Logging.logger().warning(message);
throw new UnsupportedOperationException(message);
}
}
/**
* Export the placemark to KML as a {@code } element. The {@code output} object will receive the data.
* This object must be one of: java.io.Writer java.io.OutputStream javax.xml.stream.XMLStreamWriter
*
* @param output Object to receive the generated KML.
*
* @throws XMLStreamException If an exception occurs while writing the KML
* @throws IOException if an exception occurs while exporting the data.
* @see #export(String, Object)
*/
protected void exportAsKML(Object output) throws IOException, XMLStreamException
{
XMLStreamWriter xmlWriter = null;
XMLOutputFactory factory = XMLOutputFactory.newInstance();
boolean closeWriterWhenFinished = true;
if (output instanceof XMLStreamWriter)
{
xmlWriter = (XMLStreamWriter) output;
closeWriterWhenFinished = false;
}
else if (output instanceof Writer)
{
xmlWriter = factory.createXMLStreamWriter((Writer) output);
}
else if (output instanceof OutputStream)
{
xmlWriter = factory.createXMLStreamWriter((OutputStream) output);
}
if (xmlWriter == null)
{
String message = Logging.getMessage("Export.UnsupportedOutputObject");
Logging.logger().warning(message);
throw new IllegalArgumentException(message);
}
xmlWriter.writeStartElement("Placemark");
xmlWriter.writeStartElement("name");
xmlWriter.writeCharacters(this.getLabelText());
xmlWriter.writeEndElement();
xmlWriter.writeStartElement("visibility");
xmlWriter.writeCharacters(kmlBoolean(this.isVisible()));
xmlWriter.writeEndElement();
String shortDescription = (String) getValue(AVKey.SHORT_DESCRIPTION);
if (shortDescription != null)
{
xmlWriter.writeStartElement("Snippet");
xmlWriter.writeCharacters(shortDescription);
xmlWriter.writeEndElement();
}
String description = (String) getValue(AVKey.BALLOON_TEXT);
if (description != null)
{
xmlWriter.writeStartElement("description");
xmlWriter.writeCharacters(description);
xmlWriter.writeEndElement();
}
final PointPlacemarkAttributes normalAttributes = getAttributes();
final PointPlacemarkAttributes highlightAttributes = getHighlightAttributes();
// Write style map
if (normalAttributes != null || highlightAttributes != null)
{
xmlWriter.writeStartElement("StyleMap");
exportAttributesAsKML(xmlWriter, KMLConstants.NORMAL, normalAttributes);
exportAttributesAsKML(xmlWriter, KMLConstants.HIGHLIGHT, highlightAttributes);
xmlWriter.writeEndElement(); // StyleMap
}
// Write geometry
xmlWriter.writeStartElement("Point");
xmlWriter.writeStartElement("extrude");
xmlWriter.writeCharacters(kmlBoolean(isLineEnabled()));
xmlWriter.writeEndElement();
final String altitudeMode = KMLExportUtil.kmlAltitudeMode(getAltitudeMode());
xmlWriter.writeStartElement("altitudeMode");
xmlWriter.writeCharacters(altitudeMode);
xmlWriter.writeEndElement();
final String coordString = String.format(Locale.US, "%f,%f,%f",
position.getLongitude().getDegrees(),
position.getLatitude().getDegrees(),
position.getElevation());
xmlWriter.writeStartElement("coordinates");
xmlWriter.writeCharacters(coordString);
xmlWriter.writeEndElement();
xmlWriter.writeEndElement(); // Point
xmlWriter.writeEndElement(); // Placemark
xmlWriter.flush();
if (closeWriterWhenFinished)
xmlWriter.close();
}
/**
* Export PointPlacemarkAttributes as KML Style element.
*
* @param xmlWriter Writer to receive the Style element.
* @param styleType The type of style: normal or highlight. Value should match either {@link KMLConstants#NORMAL}
* or {@link KMLConstants#HIGHLIGHT}
* @param attributes Attributes to export. The method takes no action if this parameter is null.
*
* @throws XMLStreamException if exception occurs writing XML.
* @throws IOException if exception occurs exporting data.
*/
private void exportAttributesAsKML(XMLStreamWriter xmlWriter, String styleType, PointPlacemarkAttributes attributes)
throws XMLStreamException, IOException
{
if (attributes != null)
{
xmlWriter.writeStartElement("Pair");
xmlWriter.writeStartElement("key");
xmlWriter.writeCharacters(styleType);
xmlWriter.writeEndElement();
attributes.export(KMLConstants.KML_MIME_TYPE, xmlWriter);
xmlWriter.writeEndElement(); // Pair
}
}
}