Package gov.nasa.worldwind.render

Source Code of gov.nasa.worldwind.render.PointPlacemark

/*
* 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.Position;
import gov.nasa.worldwind.geom.Vec4;
import gov.nasa.worldwind.layers.Layer;
import gov.nasa.worldwind.ogc.kml.KMLConstants;
import gov.nasa.worldwind.ogc.kml.impl.KMLExportUtil;
import static gov.nasa.worldwind.ogc.kml.impl.KMLExportUtil.kmlBoolean;
import gov.nasa.worldwind.pick.PickSupport;
import gov.nasa.worldwind.pick.PickedObject;
import gov.nasa.worldwind.util.*;
import java.awt.Color;
import java.awt.Font;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.geom.Rectangle2D;
import java.io.IOException;
import java.io.OutputStream;
import java.io.Writer;
import java.net.URL;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import javax.media.opengl.GL;
import javax.media.opengl.GL2;
import javax.xml.stream.XMLOutputFactory;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.XMLStreamWriter;

/**
* 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.
* <p/>
* 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()}.
* <p/>
* This class implements and extends the functionality of a KML <i>Point</i>.
* <p/>
* 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 1171 2013-02-11 21:45:02Z dcollins $
*/
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<String, WWTexture> textures = new HashMap<String, WWTexture>(); // 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: <ul> <li><b>@link WorldWind#CLAMP_TO_GROUND}</b>
     * -- the point is placed on the terrain at the latitude and longitude of its position.</li> <li><b>@link
     * WorldWind#RELATIVE_TO_GROUND}</b> -- the point is placed above the terrain at the latitude and longitude of its
     * position and the distance specified by its elevation.</li> <li><b>{@link WorldWind#ABSOLUTE}</b> -- the point is
     * placed at its specified position. </ul>
     *
     * @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.
     * <p/>
     * 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 <code>Renderable</code> 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.
     * <p/>
     * 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 <code>PickSupport</code> may not be the one associated with this instance. During batch
     * picking the <code>PickSupport</code> 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);
        if (localUrl != 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:
     * <p/>
     * <pre>
     * Format                                         Supported output object types
     * ================================================================================
     * KML (application/vnd.google-earth.kml+xml)     java.io.Writer
     *                                                java.io.OutputStream
     *                                                javax.xml.stream.XMLStreamWriter
     * </pre>
     *
     * @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 <Placemark>} 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
        }
    }
}
TOP

Related Classes of gov.nasa.worldwind.render.PointPlacemark

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.