Click or drag to resize

Visualization with Insight3D®

Tracking Graphics is a library specialized for use with Tracking Library allowing entities to be visualized in Insight3D®. It provides a framework for creating and updating graphics primitives for entities declaratively. This documentation assumes basic familiarity with Insight3D. The Visualization with Insight3D® topic has more details on Insight3D.

Note Note

The functionality described in this topic requires a license for the Tracking Library.

Visualizers

Insight3D provides many graphical primitives for rendering objects in a 3D scene. Tracking Graphics provides an approach for synchronizing these primitives with entities, providing a cohesive picture of the data, while also addressing the threading differences between Tracking Library (which is designed for multi-threading) and Insight3D (which is a single-threaded renderer).

Visualization is driven by classes derived from EntityVisualizer<TEntity>, each of which take an EntitySet<TEntity> and create corresponding graphical primitives for the entities in the set. When a set's content changes, the visualizer will update the primitives to match. Visualizers for points, labels, markers, models and entity history are included by default, and new visualizers can be created by deriving from EntityVisualizer<TEntity>.

Suppose we wanted to display a point of a certain color and size for each entity in a EntitySet<TEntity>. The following code sample shows how to use a PointVisualizer<TEntity> to do this:

Java
PointVisualizer<ExampleEntity> visualizer = new PointVisualizer<>(new TypeLiteral<ExampleEntity>() {});
visualizer.setEntities(entities);
visualizer.setPixelSize(6.0f);
visualizer.setColor(Color.WHITE);
visualizer.setDisplayOutline(true);
visualizer.setOutlineColor(Color.BLACK);
visualizer.setOutlineWidth(4.0f);

Creating and configuring the visualizer determines how entities will be visualized, but we must also call update in order for the entities to be rendered. This is normally done by subscribing to the TimeChanged (add / remove) event and performing the update there within a transaction.

Java
EventHandler<TimeChangedEventArgs> handler = EventHandler.of(this::sceneManagerTimeChanged);
SceneManager.addTimeChanged(handler);
Java
private void sceneManagerTimeChanged(Object sender, TimeChangedEventArgs e) {
    context.doTransactionally(Action1.of(visualizer::update));
}

The PointVisualizer<TEntity> will create a PointBatchPrimitive and then synchronize the primitive based on the current state of the entities in the EntitySet<TEntity>.

As a result of how the visualizer was configured, it will render a white point outlined in black at each entity's location, similar to the image below. As entities are added, removed or updated, the display will automatically reflect these changes every time update is called.

Point Visualizer

Note that the above example assumes that Insight3D is animating, meaning the Time (get / set) property is changing, which causes our event handler to be called, which then updates the visualizer. If it is not animating, then update can be called manually whenever it is most convenient. Normally, this is immediately before SceneManager.render or Scene.render is called.

Labels

Other visualizers work in much the same way as PointVisualizer<TEntity>. LabelVisualizer<TEntity> draws a label for each entity in an entity set, as shown in the following code sample:

Java
LabelVisualizer<ExampleEntity> visualizer = new LabelVisualizer<>(new TypeLiteral<ExampleEntity>() {});
visualizer.setEntities(entities);
visualizer.setFont(new GraphicsFont("MS Sans Serif", 12, FontStyle.REGULAR, true));
visualizer.setColor(Color.YELLOW);
visualizer.setOutlineColor(Color.BLACK);
visualizer.setCallback(LabelCallback.of((transaction, entity) -> entity.getCallSign()));
Label Visualizer

The only significant difference in the above code is the use of the Callback (get / set) property for retrieving the label text associated with each entity. Whenever the visualizer needs to update the entity label, it will call this delegate, passing it both the entity and a transaction to read entity values with. This allows each entity to have a unique label, while still efficiently rendering all entities in the set together.

Markers

MarkerVisualizer<TEntity> is similar to other visualizers in that it relies on a callback to retrieve specific textures on a per-entity basis.

Java
MarkerVisualizer<ExampleEntity> visualizer = new MarkerVisualizer<>(new TypeLiteral<ExampleEntity>() {});
visualizer.setEntities(entities);
visualizer.setSize(new DimensionF(24.0f, 24.0f));
visualizer.setCallback(MarkerCallback.of(this::getEntityMarker));
Java
private Texture2D getEntityMarker(Transaction transaction, ExampleEntity entity) {
    String symbol = entity.getSymbolId().getValue(transaction);

    Texture2D texture = m_textures.get(symbol);
    if (texture == null) {
        texture = SceneManager.getTextures().fromUri(getMarkerUriFromSymbol(symbol));
        m_textures.put(symbol, texture);
    }

    return texture;
}

private Map<String, Texture2D> m_textures = new HashMap<>();

In the above example, GetEntityMarker returns the same texture for all entities that share a common texture. This is important for efficiency. A new instance of Texture2D should not be created every time the function is called, which would be inefficient.

Marker Visualizer
Models

ModelVisualizer<TEntity> is slightly different from the above visualizers. The callback must construct a ModelPrimitive, which loads a model. Because a single ModelPrimitive in Insight3D can only be used for a single entity, multiple primitives are needed. The primitive's ReferenceFrame (get / set), Display (get / set), DisplayCondition (get / set), Color (get / set), Scale (get / set), Tag (get / set), Orientation (get / set), and Position (get / set) properties will be modified by the ModelVisualizer<TEntity>. So, they should not be set explicitly in the callback. All other properties and methods, such as those that load a new model file or adjust articulations, can be used normally.

Java
ModelVisualizer<ExampleEntity> visualizer = new ModelVisualizer<>(new TypeLiteral<ExampleEntity>() {});
visualizer.setEntities(entities);
visualizer.setCallback(ModelCallback.of(this::getEntityModel));
Java
private ModelPrimitive getEntityModel(Transaction transaction, ExampleEntity entity) {
    String symbol = entity.getSymbolId().getValue(transaction);

    ModelPrimitive model = m_models.get(symbol);
    if (model == null) {
        model = new ModelPrimitive(getModelUriFromSymbol(symbol));
        m_models.put(symbol, model);
    }

    return model;
}

private HashMap<String, ModelPrimitive> m_models = new HashMap<>();

Model Visualizer
Sensor Volumes

SensorFieldOfViewVisualizer<TEntity> makes it easy to visualize sensors associated with entities, including sensors that change shape and pointing with time. The volume to display is obtained from the entity using the IEntitySensorFieldOfView interface. It is located and oriented according to the IEntityPosition and IEntityOrientation interfaces.

Java
SensorFieldOfViewVisualizer<SensorEntity> visualizer = new SensorFieldOfViewVisualizer<>(new TypeLiteral<SensorEntity>() {});
visualizer.setEntities(entities);
visualizer.setVolumeColor(new Color(0, 0, 255, 100));
visualizer.setVolumeOutlineColor(Color.BLUE);
visualizer.setFootprintOutlineColor(Color.BLUE);
visualizer.setFootprintInteriorColor(new Color(128, 128, 128, 100));
Sensor Visualizer
History

All the visualizers discussed so far visualize the current entity state, such as its current position or orientation. However, this does not have to be the case. Two visualizers are provided for entity history visualization. Rather than visualizing the current entity position, these visualizers use historical information to render graphics primitives. WaypointVisualizer<TEntity> draws points for each history position. HistoryVisualizer<TEntity> draws a line connecting those points. Using both visualizers together gives a complete picture of where an entity has been.

Java
HistoryGenerator<ExampleEntity> historyGenerator =
        new HistoryGenerator<>(new TypeLiteral<ExampleEntity>() {}, entities, Duration.fromMinutes(1.0));

HistoryVisualizer<ExampleEntity> historyVisualizer = new HistoryVisualizer<>(new TypeLiteral<ExampleEntity>() {});
historyVisualizer.setHistoryGenerator(historyGenerator);
historyVisualizer.setEntities(entities);
historyVisualizer.setColor(Color.YELLOW);
historyVisualizer.setWidth(2.0f);

WaypointVisualizer<ExampleEntity> waypointVisualizer = new WaypointVisualizer<>(new TypeLiteral<ExampleEntity>() {});
waypointVisualizer.setHistoryGenerator(historyGenerator);
waypointVisualizer.setEntities(entities);
waypointVisualizer.setPixelSize(5.0f);
Color lightPink = new Color(0xFFB6C1);
waypointVisualizer.setColor(lightPink);
waypointVisualizer.setDisplayOutline(true);
waypointVisualizer.setOutlineColor(Color.RED);
waypointVisualizer.setOutlineWidth(3.0f);

Both classes rely on the HistoryGenerator<TEntity> class to manage track history. A single HistoryGenerator<TEntity> can be used for multiple visualizers, or for custom visualizers that need entity position history. HistoryGenerator<TEntity> has a HistoryLength (get / set) property which is a Duration specifying how much history to keep in memory. Any data points before the current Time (get / set), minus the configured history length, are removed. A longer history length means more history can be displayed, but also means that more memory and processing power will be used to keep the additional data.

History Visualizer
Picking

Picking allows users to select and interact with objects in the 3D scene. The pick method takes a normal Insight3D PickResult and returns the collection of entities associated with the result.

Java
List<PickResult> pickResults = scene.pick(x, y);
for (PickResult pickResult : pickResults) {
    Iterable<ExampleEntity> pickedEntities = visualizer.pick(pickResult);
}
Filters and Entity Providers

When dealing with many entities which should be distinguished in the visualization, the filtering capabilities of Tracking Library can be used to group entities into distinct sets, which can then be used to apply visual appearances. The Filtering topic has more detail on filtering entities.

A visualizer's Entities (get / set) property can be directly configured with an EntitySet<TEntity> of a filter. By connecting different sets of MatchingEntities (get) from different EntityFilter<TEntity> objects, we can visualize those sets differently. For example, the following code sample uses filters to categorize entities based on their affiliation, i.e. whether they are friendly, neutral, hostile or unknown. In this case, as in most cases when using filtering with visualizers, we configure the filter chain's MatchingStrategy (get / set) property to be MatchingStrategy.FIRST, because we want each entity to be managed by only one visualizer.

Java
HistoryGenerator<ExampleEntity> historyGenerator =
        new HistoryGenerator<>(new TypeLiteral<ExampleEntity>() {}, entities, Duration.fromMinutes(1.0));
final ArrayList<EntityVisualizer<ExampleEntity>> visualizers = new ArrayList<>();
EntityFilterChain<ExampleEntity> filterChain = new EntityFilterChain<>(entities, MatchingStrategy.FIRST);

// Make friendly filter that displays as blue
DelegateEntityFilter<ExampleEntity> friendlyFilter = new DelegateEntityFilter<>(context,
        IsMatchCallback.of((transaction, entity) -> entity.getAffiliation().getValue(transaction) == Force.FRIENDLY));
filterChain.getFilters().add(friendlyFilter);
addVisualizers(friendlyFilter.getMatchingEntities(), visualizers, historyGenerator, Color.BLUE);

// Make hostile filter that displays as red
DelegateEntityFilter<ExampleEntity> hostileFilter = new DelegateEntityFilter<>(context,
        IsMatchCallback.of((transaction, entity) -> entity.getAffiliation().getValue(transaction) == Force.HOSTILE));
filterChain.getFilters().add(hostileFilter);
addVisualizers(hostileFilter.getMatchingEntities(), visualizers, historyGenerator, Color.RED);

// Make neutral filter that displays as green
DelegateEntityFilter<ExampleEntity> neutralFilter = new DelegateEntityFilter<>(context,
        IsMatchCallback.of((transaction, entity) -> entity.getAffiliation().getValue(transaction) == Force.NEUTRAL));
filterChain.getFilters().add(neutralFilter);
addVisualizers(neutralFilter.getMatchingEntities(), visualizers, historyGenerator, ColorHelper.GREEN);

// Make anything left (unknown) display as yellow
addVisualizers(filterChain.getHomelessEntities(), visualizers, historyGenerator, Color.YELLOW);

filterChain.applyChanges();

// Subscribe to the time changed event so we can update visualizers
EventHandler<TimeChangedEventArgs> onTimeChanged = EventHandler.of((sender, args) -> {
    context.doTransactionally(Action1.of((transaction) -> {
        for (EntityVisualizer<ExampleEntity> visualizer : visualizers) {
            visualizer.update(transaction);
        }
    }));
});
SceneManager.addTimeChanged(onTimeChanged);
Java
private void addVisualizers(EntitySet<ExampleEntity> entities,
                            ArrayList<EntityVisualizer<ExampleEntity>> visualizers,
                            HistoryGenerator<ExampleEntity> historyGenerator,
                            Color color) {
    // Create a model, marker, point, label and track history for each entity.
    // Layer the visualizers using DistanceDisplayConditions such that
    // models and history are shown up to 6000 meters away from the camera, markers
    // then turn on and stay until 100,000,000 meters, followed by Points
    // which stay on forever.  Labels are on from 100 to 50,000,000 meters.

    ModelVisualizer<ExampleEntity> modelVisualizer = new ModelVisualizer<>(new TypeLiteral<ExampleEntity>() {});
    modelVisualizer.setEntities(entities);
    modelVisualizer.setCallback(ModelCallback.of(this::getEntityModel));
    modelVisualizer.setColor(color);
    modelVisualizer.setScale(1.0);
    modelVisualizer.setDisplayCondition(new DistanceDisplayCondition(0.0, 6000.0));
    visualizers.add(modelVisualizer);

    MarkerVisualizer<ExampleEntity> markerVisualizer = new MarkerVisualizer<>(new TypeLiteral<ExampleEntity>() {});
    markerVisualizer.setEntities(entities);
    markerVisualizer.setCallback(MarkerCallback.of(this::getEntityMarker));
    markerVisualizer.setColor(Color.WHITE);
    markerVisualizer.setDisplayCondition(new DistanceDisplayCondition(6000.0, 100000000.0));
    markerVisualizer.setSize(new DimensionF(24.0f, 24.0f));
    visualizers.add(markerVisualizer);

    PointVisualizer<ExampleEntity> pointVisualizer = new PointVisualizer<>(new TypeLiteral<ExampleEntity>() {});
    pointVisualizer.setEntities(entities);
    pointVisualizer.setColor(color);
    pointVisualizer.setDisplayCondition(new DistanceDisplayCondition(100000000.0, Double.MAX_VALUE));
    visualizers.add(pointVisualizer);

    LabelVisualizer<ExampleEntity> labelVisualizer = new LabelVisualizer<>(new TypeLiteral<ExampleEntity>() {});
    labelVisualizer.setEntities(entities);
    labelVisualizer.setCallback(LabelCallback.of(this::getEntityLabel));
    labelVisualizer.setColor(color);
    labelVisualizer.setOutlineColor(Color.BLACK);
    labelVisualizer.setDisplayCondition(new DistanceDisplayCondition(100.0, 50000000.0));
    labelVisualizer.setOrigin(Origin.BOTTOM_CENTER);
    labelVisualizer.setPixelOffset(new PointF(0.0f, 12.0f));
    visualizers.add(labelVisualizer);

    HistoryVisualizer<ExampleEntity> historyVisualizer = new HistoryVisualizer<>(new TypeLiteral<ExampleEntity>() {});
    historyVisualizer.setEntities(entities);
    historyVisualizer.setHistoryGenerator(historyGenerator);
    historyVisualizer.setColor(color);
    historyVisualizer.setWidth(1.0f);
    historyVisualizer.setDisplayCondition(new DistanceDisplayCondition(0.0, 6000.0));
    visualizers.add(historyVisualizer);
}

Tracking Visualization Filter Example

The same approach can be used to separate entities based on more complex analysis results. Using AccessEntityFilter<TEntity> for example, you could make all entities with line of sight to a specific point on the ground green and entities without line of sight could be red, or not shown at all.

Camera Control

ViewFromTo<TEntity> and ViewEntityFromOffset<TEntity> are classes which can be used to move the camera in order to maintain a specific view based on the position of one or more entities. ViewEntityFromOffset<TEntity> tracks a single entity from a specified offset, as shown in the following code sample:

Java
ViewEntityFromOffset<ExampleEntity> view = new ViewEntityFromOffset<>(new TypeLiteral<ExampleEntity>() {});
view.setCamera(scene.getCamera());
view.setEntity(entityToView);
view.setOffset(new Cartesian(0.0, 0.0, 100.0));
view.setCentralBody(CentralBodiesFacet.getFromContext().getEarth());

context.doTransactionally(Action1.of(view::update));

By default, Insight3D allows the user to move the camera with the mouse. ViewEntityFromOffset<TEntity> allows the user to zoom in and out as well as rotate around the specified entity, while ensuring the entity remains the focus point of the camera. In order to lock the view to a specific offset, default mouse navigation must be disabled. See MouseOptions for details.

ViewFromTo<TEntity> modifies the camera to view from one entity or Point to another.

Java
ViewFromTo<ExampleEntity> viewFromTo = new ViewFromTo<>(new TypeLiteral<ExampleEntity>() {});
viewFromTo.setCamera(scene.getCamera());
viewFromTo.setTo(entityToView);
viewFromTo.setFrom(observingPoint);
viewFromTo.setShape(CentralBodiesFacet.getFromContext().getEarth().getShape());

context.doTransactionally(Action1.of(viewFromTo::update));

As mentioned above, mouse camera control can create undesired behavior when trying to lock the view to both a specific to and from point. As a result, disabling mouse camera control is recommended while this view is active. See MouseOptions for details.

Both of these classes only modify camera properties related to Position (get / set) and ReferencePoint (get / set). All other camera properties can be changed as desired.

Animation Time

It's important that the Insight3D animation time match the time of the data that is being visualized. For example, whether the objects are drawn in sunlight or darkness will be affected by the time, as well as the position of the sun and other planets. The following code sample configures the animation so that the time is always the same as your system clock:

Java
SceneManager.setAnimation(new RealTimeAnimation());
SceneManager.getAnimation().playForward();

If you are working with simulated data with an interval that spans a different time or if your application is driven by a different clock, you can call the setTime method to set the time manually. See the Animation topic for more details.

Custom Visualizers

While it is possible to create additional custom entity graphics using the Insight3D update loop directly, separating out the code into a custom visualizer implementation makes it more easily reusable. In order to implement a new visualizer, derive from EntityVisualizer<TEntity>. The following code sample draws a drop-down line from the entity location to the CentralBody below. It also displays the altitude of the entity at the midpoint of the line. By extending EntityVisualizer<TEntity>, this class can be easily reused in other Tracking Library applications.

Java
/**
 * This {@link EntityVisualizer} creates a line dropping
 * down from each entity's location to the ground below.  It also creates
 * a label indicating the altitude of the entity at the halfway point
 * of the line.
 * @param <TEntity> The type of entity.
 */
public class ExampleDropDownVisualizer<TEntity extends IEntityIdentifier & IEntityPosition> extends EntityVisualizer<TEntity> {
    /**
     * Creates a new instance.  Entities and central body properties
     * must be explicitly set.
     * @param typeLiteralTEntity A TypeLiteral object representing the generic type {@code TEntity}.
     */
    public ExampleDropDownVisualizer(TypeLiteral<TEntity> typeLiteralTEntity) {
        super(typeLiteralTEntity);

        m_lines = new PolylinePrimitive(PolylineType.LINES, SetHint.FREQUENT);
        m_lines.setColor(Color.WHITE);
        m_lines.setOutlineColor(Color.BLACK);

        GraphicsFont font = new GraphicsFont("MS Sans Serif", 10, FontStyle.REGULAR, true);
        m_labels = new TextBatchPrimitive(font, SetHint.FREQUENT);
        m_labels.setColor(Color.WHITE);
        m_labels.setOutlineColor(Color.BLACK);

        IEntityPositionDescriptor descriptor = EntityDescriptor.getDefault(typeLiteralTEntity).get(new TypeLiteral<IEntityPositionDescriptor>() {});
        m_lines.setReferenceFrame(descriptor.getPositionReferenceFrame());
        m_labels.setReferenceFrame(descriptor.getPositionReferenceFrame());
    }

    @Override
    protected void dispose(boolean disposing) {
        if (disposing && m_lines != null) {
            clear();
            m_lines.dispose();
            m_labels.dispose();
            m_lines = null;
            m_labels = null;
        }
    }

    /**
     * Gets the width of the line in pixels.
     */
    public final float getWidth() {
        return m_lines.getWidth();
    }

    /**
     * Sets the width of the line in pixels.
     */
    public final void setWidth(float value) {
        m_lines.setWidth(value);
    }

    /**
     * Gets whether to outline each line.
     */
    public final boolean getDisplayOutline() {
        return m_lines.getDisplayOutline();
    }

    /**
     * Sets whether to outline each line.
     */
    public final void setDisplayOutline(boolean value) {
        m_lines.setDisplayOutline(value);
    }

    /**
     * Gets the color of the outline for line and label.
     */
    public final Color getOutlineColor() {
        return m_lines.getOutlineColor();
    }

    /**
     * Sets the color of the outline for line and label.
     */
    public final void setOutlineColor(Color value) {
        m_labels.setOutlineColor(value);
        m_lines.setOutlineColor(value);
    }

    /**
     * Gets the translucency of the outline.
     * 0 is opaque and 1 is transparent.
     */
    public final float getOutlineTranslucency() {
        return m_lines.getOutlineTranslucency();
    }

    /**
     * Sets the translucency of the outline.
     * 0 is opaque and 1 is transparent.
     */
    public final void setOutlineTranslucency(float value) {
        m_labels.setOutlineTranslucency(value);
        m_lines.setOutlineTranslucency(value);
    }

    /**
     * Gets the width of the outline for the line.
     */
    public final float getOutlineWidth() {
        return m_lines.getOutlineWidth();
    }

    /**
     * Sets the width of the outline for the line.
     */
    public final void setOutlineWidth(float value) {
        m_lines.setOutlineWidth(value);
    }

    /**
     * Gets the CentralBody which the lines
     * drop down onto.
     */
    public final CentralBody getCentralBody() {
        return m_centralBody;
    }

    /**
     * Sets the CentralBody which the lines
     * drop down onto.
     */
    public final void setCentralBody(CentralBody value) {
        m_centralBody = value;
    }

    /**
     * Gets the entities to visualize.
     */
    @Override
    public EntitySet<TEntity> getEntities() {
        return m_entities;
    }

    /**
     * Sets the entities to visualize.
     */
    @Override
    public void setEntities(EntitySet<TEntity> value) {
        m_entities = value;
    }

    /**
     * Gets the color of the drop line and text.
     */
    @Override
    public Color getColor() {
        return m_lines.getColor();
    }

    /**
     * Sets the color of the drop line and text.
     */
    @Override
    public void setColor(Color value) {
        m_labels.setColor(value);
        m_lines.setColor(value);
    }

    /**
     * Gets the translucency.
     * 0 is opaque and 1 is transparent.
     */
    @Override
    public float getTranslucency() {
        return m_lines.getTranslucency();
    }

    /**
     * Sets the translucency.
     * 0 is opaque and 1 is transparent.
     */
    @Override
    public void setTranslucency(float value) {
        m_labels.setTranslucency(value);
        m_lines.setTranslucency(value);
    }

    /**
     * Gets whether the droplines are displayed.
     */
    @Override
    public boolean getDisplay() {
        return m_lines.getDisplay();
    }

    /**
     * Sets whether the droplines are displayed.
     */
    @Override
    public void setDisplay(boolean value) {
        m_labels.setDisplay(value);
        m_lines.setDisplay(value);
    }

    /**
     * Gets the display condition.
     */
    @Override
    public DisplayCondition getDisplayCondition() {
        return m_lines.getDisplayCondition();
    }

    /**
     * Sets the display condition.
     */
    @Override
    public void setDisplayCondition(DisplayCondition value) {
        m_labels.setDisplayCondition(value);
        m_lines.setDisplayCondition(value);
    }

    /**
     * Updates the underlying primitives to reflect the latest entity states.
     * @param transaction The transaction to use.
     * @exception ArgumentNullException
     * Thrown if {@code transaction} is {@code null}.
     * @exception PropertyInvalidException
     * Thrown if {@code Entities} ({@link #getEntities get} / {@link #setEntities set}) is {@code null}.
     * @exception PropertyInvalidException
     * Thrown if {@code CentralBody} ({@link #getCentralBody get} / {@link #setCentralBody set}) is {@code null}.
     */
    @Override
    public void update(Transaction transaction) {
        if (transaction == null) {
            throw new ArgumentNullException("transaction");
        }

        PropertyInvalidException.validateNonNull(getEntities(), "Entities");
        PropertyInvalidException.validateNonNull(getCentralBody(), "CentralBody");

        int entityCount = getEntities().getCount(transaction);
        if (getDisplay() && entityCount > 0) {
            if (m_linePoints == null) {
                SceneManager.getPrimitives().add(m_lines);
                SceneManager.getPrimitives().add(m_labels);
            }

            if (m_linePoints.length != entityCount * 2) {
                m_linePoints = Arrays.copyOf(m_linePoints, entityCount * 2);
            }

            if (m_labelPositions.length != entityCount) {
                m_labelPositions = Arrays.copyOf(m_labelPositions, entityCount);
            }

            if (m_labelText.length != entityCount) {
                m_labelText = Arrays.copyOf(m_labelText, entityCount);
            }

            int index = 0;
            int lineIndex = 0;
            for (TEntity entity : getEntities().getEntities(transaction)) {
                // Compute dropline
                Cartesian position = entity.getPosition().getValue(transaction);
                m_linePoints[lineIndex] = position;
                lineIndex++;

                Cartesian surfacePosition = getCentralBody().getShape().surfaceProjection(position);
                m_linePoints[lineIndex] = surfacePosition;
                lineIndex++;

                // Compute dropline mid-point
                Cartesian labelPosition = position.add(surfacePosition).divide(2.0);
                m_labelPositions[index] = labelPosition;

                // Compute height and generate string in km
                double height = getCentralBody().getShape().cartesianToCartographic(position).getHeight();

                m_labelText[index] = heightFormat.format(height / 1000.0) + " km";

                index++;
            }

            m_lines.set(Arrays.asList(m_linePoints));
            m_labels.set(Arrays.asList(m_labelPositions), Arrays.asList(m_labelText));
        } else {
            // Not displaying anything, so clean up.
            clear();
        }
    }

    /**
     * Given a {@link PickResult}, returns the entities that were picked.
     * @param pickResult The result of the Insight3D pick.
     * @return The list of picked entities.
     * @exception ArgumentNullException
     * Thrown if {@code pickResult} is {@code null}.
     */
    @Override
    public Iterable<TEntity> pick(PickResult pickResult) {
        if (pickResult == null) {
            throw new ArgumentNullException("pickResult");
        }

        // pickResult.getPosition() is currently always in the earth
        // fixed frame. We need to transform to the entities'
        // reference frame for comparison.
        ReferenceFrame fixedFrame = CentralBodiesFacet.getFromContext().getEarth().getFixedFrame();

        ReferenceFrameEvaluator geometryTransformer = GeometryTransformer.getReferenceFrameTransformation(fixedFrame, m_lines.getReferenceFrame());

        KinematicTransformation transformer = geometryTransformer.evaluate(SceneManager.getTime());
        final Cartesian pickPosition = transformer.transform(pickResult.getPosition());

        PickData<TEntity> pickData = new PickData<>();
        for (Object pick : pickResult.getObjects()) {
            if (pick == m_lines || pick == m_labels) {
                getEntities().getContext().doTransactionally(Action1.of(transaction -> {
                    for (TEntity entity : getEntities().getEntities(transaction)) {
                        Cartesian position = entity.getPosition().getValue(transaction);
                        double distance = Cartesian.subtract(position, pickPosition).getMagnitude();
                        if (distance < pickData.smallestDistance) {
                            pickData.smallestDistance = distance;
                            pickData.closestEntity = entity;
                        }
                    }
                }));
            }
        }

        ArrayList<TEntity> pickedEntities = new ArrayList<>();

        if (pickData.closestEntity != null) {
            pickedEntities.add(pickData.closestEntity);
        }

        return pickedEntities;
    }

    /**
     * Helper class for storing information while searching for the pick.
     */
    private static class PickData<TEntity extends IEntityIdentifier & IEntityPosition> {
        public TEntity closestEntity = null;
        public double smallestDistance = Double.MAX_VALUE;
    }

    /**
     * Removes all primitives and cleans up.
     */
    private final void clear() {
        if (m_linePoints != null) {
            SceneManager.getPrimitives().remove(m_lines);
            SceneManager.getPrimitives().remove(m_labels);
            m_linePoints = null;
            m_labelPositions = null;
            m_labelText = null;
        }
    }

    private static final DecimalFormat heightFormat = new DecimalFormat("0.00");
    private Cartesian[] m_linePoints;
    private Cartesian[] m_labelPositions;
    private PolylinePrimitive m_lines;
    private String[] m_labelText;
    private TextBatchPrimitive m_labels;
    private CentralBody m_centralBody;
    private EntitySet<TEntity> m_entities;
}