Click or drag to resize

Visualization with Insight3D®

Tracking Library includes a specialized library, Tracking Graphics, which visualizes entities in Insight3D®, by providing 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 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:

C#
var visualizer = new PointVisualizer<ExampleEntity>
{
    Entities = entities,
    PixelSize = 6.0f,
    Color = Color.White,
    DisplayOutline = true,
    OutlineColor = Color.Black,
    OutlineWidth = 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 event and performing the update there within a transaction.

C#
SceneManager.TimeChanged += SceneManagerTimeChanged;
C#
private void SceneManagerTimeChanged(object sender, TimeChangedEventArgs e)
{
    context.DoTransactionally(transaction => visualizer.Update(transaction));
}

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 property is changing, which causes our event handler to be called, which updates the visualizer. If it is not animating, then Update can be called manually whenever it is most convenient; normally this is immediately before SceneManagerRender or SceneRender 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:

C#
var visualizer = new LabelVisualizer<ExampleEntity>
{
    Entities = entities,
    Font = new GraphicsFont("MS Sans Serif", 12, FontStyle.Regular, true),
    Color = Color.Yellow,
    OutlineColor = Color.Black,
    Callback = (transaction, entity) => entity.CallSign
};
Label Visualizer

The only significant difference in the above code is the use of the Callback 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, relying on a callback to retrieve specific textures on a per-entity basis.

C#
var visualizer = new MarkerVisualizer<ExampleEntity>
{
    Entities = entities,
    Size = new SizeF(24.0f, 24.0f),
    Callback = GetEntityMarker
};
C#
private Texture2D GetEntityMarker(Transaction transaction, ExampleEntity entity)
{
    string symbol = entity.SymbolId.GetValue(transaction);

    Texture2D texture;
    if (!m_textures.TryGetValue(symbol, out texture))
    {
        texture = SceneManager.Textures.FromUri(GetMarkerUriFromSymbol(symbol));
        m_textures.Add(symbol, texture);
    }

    return texture;
}

private readonly Dictionary<string, Texture2D> m_textures = new Dictionary<string, Texture2D>();

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 actually must constructs 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, Display, DisplayCondition, Color, Scale, Tag, Orientation, and Position 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.

C#
var visualizer = new ModelVisualizer<ExampleEntity>
{
    Entities = entities,
    Callback = GetEntityModel
};
C#
private ModelPrimitive GetEntityModel(Transaction transaction, ExampleEntity entity)
{
    string symbol = entity.SymbolId.GetValue(transaction);

    ModelPrimitive model;
    if (!m_models.TryGetValue(symbol, out model))
    {
        model = new ModelPrimitive(GetModelUriFromSymbol(symbol));
        m_models.Add(symbol, model);
    }

    return model;
}

private readonly Dictionary<string, ModelPrimitive> m_models = new Dictionary<string, ModelPrimitive>();
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, and it is positioned and oriented according to the IEntityPosition and IEntityOrientation interfaces.

C#
var visualizer = new SensorFieldOfViewVisualizer<SensorEntity>
{
    Entities = entities,
    VolumeColor = Color.FromArgb(100, Color.Blue),
    VolumeOutlineColor = Color.Blue,
    FootprintOutlineColor = Color.Blue,
    FootprintInteriorColor = Color.FromArgb(100, Color.Gray)
};
Sensor Visualizer
History

All of 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.

C#
var historyGenerator = new HistoryGenerator<ExampleEntity>(entities, Duration.FromMinutes(1.0));

var historyVisualizer = new HistoryVisualizer<ExampleEntity>
{
    HistoryGenerator = historyGenerator,
    Entities = entities,
    Color = Color.Yellow,
    Width = 2.0f
};

var waypointVisualizer = new WaypointVisualizer<ExampleEntity>
{
    HistoryGenerator = historyGenerator,
    Entities = entities,
    PixelSize = 5.0f,
    Color = Color.LightPink,
    DisplayOutline = true,
    OutlineColor = Color.Red,
    OutlineWidth = 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 for custom visualizers that need entity position history. HistoryGenerator<TEntity> has a HistoryLength property which is a Duration specifying how much history to keep in memory. Any data points before the current Time, 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.

C#
Collection<PickResult> pickResults = scene.Pick(x, y);
foreach (var pickResult in pickResults)
{
    IEnumerable<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 property can be directly configured with an EntitySet<TEntity> which is the output set of a filter. By connecting different sets of MatchingEntities 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 property to be MatchingStrategy.First, because we want each entity to managed by only one visualizer.

C#
var historyGenerator = new HistoryGenerator<ExampleEntity>(entities, Duration.FromMinutes(1.0));
var visualizers = new List<EntityVisualizer<ExampleEntity>>();
var filterChain = new EntityFilterChain<ExampleEntity>(entities, MatchingStrategy.First);

// Make friendly filter that displays as blue
var friendlyFilter =
    new DelegateEntityFilter<ExampleEntity>(context,
                                            (transaction, entity) => entity.Affiliation.GetValue(transaction) == Force.Friendly);
filterChain.Filters.Add(friendlyFilter);
AddVisualizers(friendlyFilter.MatchingEntities, visualizers, historyGenerator, Color.Blue);

// Make hostile filter that displays as red
var hostileFilter =
    new DelegateEntityFilter<ExampleEntity>(context,
                                            (transaction, entity) => entity.Affiliation.GetValue(transaction) == Force.Hostile);
filterChain.Filters.Add(hostileFilter);
AddVisualizers(hostileFilter.MatchingEntities, visualizers, historyGenerator, Color.Red);

// Make neutral filter that displays as green
var neutralFilter =
    new DelegateEntityFilter<ExampleEntity>(context,
                                            (transaction, entity) => entity.Affiliation.GetValue(transaction) == Force.Neutral);
filterChain.Filters.Add(neutralFilter);
AddVisualizers(neutralFilter.MatchingEntities, visualizers, historyGenerator, Color.Green);

// Make anything left (unknown) display as yellow
AddVisualizers(filterChain.HomelessEntities, visualizers, historyGenerator, Color.Yellow);

filterChain.ApplyChanges();

// Subscribe to the time changed event so we can update visualizers
EventHandler<TimeChangedEventArgs> onTimeChanged = (sender, args) =>
{
    context.DoTransactionally(transaction =>
    {
        foreach (var visualizer in visualizers)
        {
            visualizer.Update(transaction);
        }
    });
};
SceneManager.TimeChanged += onTimeChanged;
C#
private void AddVisualizers(EntitySet<ExampleEntity> entities,
                            List<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.

    visualizers.Add(new ModelVisualizer<ExampleEntity>
    {
        Entities = entities,
        Callback = GetEntityModel,
        Color = color,
        Scale = 1.0,
        DisplayCondition = new DistanceDisplayCondition(0.0, 6000.0)
    });

    visualizers.Add(new MarkerVisualizer<ExampleEntity>
    {
        Entities = entities,
        Callback = GetEntityMarker,
        Color = Color.White,
        DisplayCondition = new DistanceDisplayCondition(6000.0, 100000000.0),
        Size = new SizeF(24.0f, 24.0f)
    });

    visualizers.Add(new PointVisualizer<ExampleEntity>
    {
        Entities = entities,
        Color = color,
        DisplayCondition = new DistanceDisplayCondition(100000000.0, double.MaxValue)
    });

    visualizers.Add(new LabelVisualizer<ExampleEntity>
    {
        Entities = entities,
        Callback = GetEntityLabel,
        Color = color,
        OutlineColor = Color.Black,
        DisplayCondition = new DistanceDisplayCondition(100.0, 50000000.0),
        Origin = Origin.BottomCenter,
        PixelOffset = new PointF(0.0f, 12.0f)
    });

    visualizers.Add(new HistoryVisualizer<ExampleEntity>
    {
        Entities = entities,
        HistoryGenerator = historyGenerator,
        Color = color,
        Width = 1.0f,
        DisplayCondition = new DistanceDisplayCondition(0.0, 6000.0)
    });
}
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:

C#
var viewEntityFromOffset = new ViewEntityFromOffset<ExampleEntity>
{
    Camera = scene.Camera,
    Entity = entityToView,
    Offset = new Cartesian(0, 0, 100),
    CentralBody = CentralBodiesFacet.GetFromContext().Earth
};

context.DoTransactionally(transaction => viewEntityFromOffset.Update(transaction));

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 sure 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.

C#
var viewFromTo = new ViewFromTo<ExampleEntity>
{
    Camera = scene.Camera
};
viewFromTo.SetTo(entityToView);
viewFromTo.SetFrom(observingPoint);
viewFromTo.Shape = CentralBodiesFacet.GetFromContext().Earth.Shape;

context.DoTransactionally(transaction => viewFromTo.Update(transaction));

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 and ReferencePoint are modified. 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:

C#
SceneManager.Animation = new RealTimeAnimation();
SceneManager.Animation.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 directly, using the Insight3D update loop, 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 shows an example implementation which 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.

C#
/// <summary>
/// This <see cref="EntityVisualizer{TEntity}"/> 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.
/// </summary>
/// <typeparam name="TEntity">The type of entity.</typeparam>
public class ExampleDropDownVisualizer<TEntity> : EntityVisualizer<TEntity>
    where TEntity : class, IEntityIdentifier, IEntityPosition
{
    /// <summary>
    /// Creates a new instance.  Entities and central body properties
    /// must be explicitly set.
    /// </summary>
    public ExampleDropDownVisualizer()
    {
        m_lines = new PolylinePrimitive(PolylineType.Lines, SetHint.Frequent)
        {
            Color = Color.White,
            OutlineColor = Color.Black
        };

        GraphicsFont font = new GraphicsFont("MS Sans Serif", 10, FontStyle.Regular, true);
        m_labels = new TextBatchPrimitive(font, SetHint.Frequent)
        {
            Color = Color.White,
            OutlineColor = Color.Black
        };

        IEntityPositionDescriptor descriptor = EntityDescriptor<TEntity>.Default.Get<IEntityPositionDescriptor>();
        m_lines.ReferenceFrame = descriptor.PositionReferenceFrame;
        m_labels.ReferenceFrame = descriptor.PositionReferenceFrame;
    }

    /// <inheritdoc />
    protected override void Dispose(bool disposing)
    {
        if (disposing && m_lines != null)
        {
            Clear();
            m_lines.Dispose();
            m_labels.Dispose();
            m_lines = null;
            m_labels = null;
        }
    }

    /// <summary>
    /// Gets or sets the width of the line in pixels.
    /// </summary>
    public float Width
    {
        get { return m_lines.Width; }
        set { m_lines.Width = value; }
    }

    /// <summary>
    /// Gets or sets whether to outline each line.
    /// </summary>
    public bool DisplayOutline
    {
        get { return m_lines.DisplayOutline; }
        set { m_lines.DisplayOutline = value; }
    }

    /// <summary>
    /// Gets or sets the color of the outline for line and label.
    /// </summary>
    public Color OutlineColor
    {
        get { return m_lines.OutlineColor; }
        set
        {
            m_labels.OutlineColor = value;
            m_lines.OutlineColor = value;
        }
    }

    /// <summary>
    /// Gets or sets the translucency of the outline.
    /// 0 is opaque and 1 is transparent.
    /// </summary>
    public float OutlineTranslucency
    {
        get { return m_lines.OutlineTranslucency; }
        set
        {
            m_labels.OutlineTranslucency = value;
            m_lines.OutlineTranslucency = value;
        }
    }

    /// <summary>
    /// Gets or sets the width of the outline for the line.
    /// </summary>
    public float OutlineWidth
    {
        get { return m_lines.OutlineWidth; }
        set { m_lines.OutlineWidth = value; }
    }

    /// <summary>
    /// Gets or sets the CentralBody which the lines
    /// drop down onto.
    /// </summary>
    public CentralBody CentralBody { get; set; }

    /// Gets or sets the entities to visualize.
    public override EntitySet<TEntity> Entities { get; set; }

    /// <summary>
    /// Gets or sets the color of the drop line and text.
    /// </summary>
    public override Color Color
    {
        get { return m_lines.Color; }
        set
        {
            m_labels.Color = value;
            m_lines.Color = value;
        }
    }

    /// <summary>
    /// Gets or sets the translucency.
    /// 0 is opaque and 1 is transparent.
    /// </summary>
    public override float Translucency
    {
        get { return m_lines.Translucency; }
        set
        {
            m_labels.Translucency = value;
            m_lines.Translucency = value;
        }
    }

    /// <summary>
    /// Gets or sets whether the droplines are displayed.
    /// </summary>
    public override bool Display
    {
        get { return m_lines.Display; }
        set
        {
            m_labels.Display = value;
            m_lines.Display = value;
        }
    }

    /// <summary>
    /// Gets or sets the display condition.
    /// </summary>
    public override DisplayCondition DisplayCondition
    {
        get { return m_lines.DisplayCondition; }
        set
        {
            m_labels.DisplayCondition = value;
            m_lines.DisplayCondition = value;
        }
    }

    /// <summary>
    /// Updates the underlying primitives to reflect the latest entity states.
    /// </summary>
    /// <param name="transaction">The transaction to use.</param>
    /// <exception cref="ArgumentNullException">
    /// Thrown if <paramref name="transaction"/> is <see langword="null"/>.
    /// </exception>
    /// <exception cref="PropertyInvalidException">
    /// Thrown if <see cref="Entities"/> is <see langword="null"/>.
    /// </exception>
    /// <exception cref="PropertyInvalidException">
    /// Thrown if <see cref="CentralBody"/> is <see langword="null"/>.
    /// </exception>
    public override void Update(Transaction transaction)
    {
        if (transaction == null)
            throw new ArgumentNullException("transaction");

        PropertyInvalidException.ValidateNonNull(Entities, "Entities");
        PropertyInvalidException.ValidateNonNull(CentralBody, "CentralBody");

        int entityCount = Entities.GetCount(transaction);
        if (Display && entityCount > 0)
        {
            if (m_linePoints == null)
            {
                SceneManager.Primitives.Add(m_lines);
                SceneManager.Primitives.Add(m_labels);
            }

            Array.Resize(ref m_linePoints, entityCount * 2);
            Array.Resize(ref m_labelPositions, entityCount);
            Array.Resize(ref m_labelText, entityCount);

            int index = 0;
            int lineIndex = 0;
            foreach (TEntity entity in Entities.GetEntities(transaction))
            {
                // Compute dropline
                Cartesian position = entity.Position.GetValue(transaction);
                m_linePoints[lineIndex] = position;
                lineIndex++;

                Cartesian surfacePosition = CentralBody.Shape.SurfaceProjection(position);
                m_linePoints[lineIndex] = (surfacePosition);
                lineIndex++;

                // Compute dropline mid-point
                Cartesian labelPosition = position.Add(surfacePosition).Divide(2);
                m_labelPositions[index] = labelPosition;

                // Compute height and generate string in km
                double height = CentralBody.Shape.CartesianToCartographic(position).Height;
                m_labelText[index] = string.Format("{0:0.00} km", height / 1000.0);

                index++;
            }

            m_lines.Set(m_linePoints);
            m_labels.Set(m_labelPositions, m_labelText);
        }
        else
        {
            // Not displaying anything, so clean up.
            Clear();
        }
    }

    /// <summary>
    /// Given a <see cref="PickResult"/>, returns the entities that were picked.
    /// </summary>
    /// <param name="pickResult">The result of the Insight3D pick.</param>
    /// <returns>The list of picked entities.</returns>
    /// <exception cref="ArgumentNullException">
    /// Thrown if <paramref name="pickResult"/> is <see langword="null"/>.
    /// </exception>
    public override IEnumerable<TEntity> Pick(PickResult pickResult)
    {
        if (pickResult == null)
            throw new ArgumentNullException("pickResult");

        // pickResult.Position is currently always in the earth
        // fixed frame. We need to transform to the entities'
        // reference frame for comparison.
        ReferenceFrame fixedFrame = CentralBodiesFacet.GetFromContext().Earth.FixedFrame;

        ReferenceFrameEvaluator geometryTransformer = GeometryTransformer.GetReferenceFrameTransformation(fixedFrame, m_lines.ReferenceFrame);

        KinematicTransformation transformer = geometryTransformer.Evaluate(SceneManager.Time);
        Cartesian pickPosition = transformer.Transform(pickResult.Position);

        TEntity closestEntity = null;
        double smallestDistance = double.MaxValue;

        foreach (object pick in pickResult.Objects)
        {
            if (pick == m_lines || pick == m_labels)
            {
                Entities.Context.DoTransactionally(transaction =>
                {
                    foreach (TEntity entity in Entities.GetEntities(transaction))
                    {
                        Cartesian position = entity.Position.GetValue(transaction);
                        double distance = (position - pickPosition).Magnitude;
                        if (distance < smallestDistance)
                        {
                            smallestDistance = distance;
                            closestEntity = entity;
                        }
                    }
                });
            }
        }

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

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

        return pickedEntities;
    }

    /// <summary>
    /// Removes all primitives and cleans up.
    /// </summary>
    private void Clear()
    {
        if (m_linePoints != null)
        {
            SceneManager.Primitives.Remove(m_lines);
            SceneManager.Primitives.Remove(m_labels);
            m_linePoints = null;
            m_labelPositions = null;
            m_labelText = null;
        }
    }

    private Cartesian[] m_linePoints;
    private Cartesian[] m_labelPositions;
    private PolylinePrimitive m_lines;
    private string[] m_labelText;
    private TextBatchPrimitive m_labels;
}