MATLAB Graphics and App Building

MATLAB graphics, data visualization, and app building

Creating a Flight Tracking Dashboard, Part 2: Developing an Aircraft Chart

Guest Writer: Ken Deeley

Ken is an application engineer in Cambridge, UK, supporting MathWorks users with their technical computing projects. Ken joined the MathWorks customer training team in 2011 after research work in mathematics with applications to motion planning for autonomous guided vehicle (AGV) robotics. Ken specializes in software development, machine learning, and financial applications, with a particular focus on graphics and app development. He enjoys training MathWorks users on best practices, working with customers on consulting projects, and capturing common customer workflows and requirements to inform future MathWorks development activities.

Overview

In the first blog post of this series, we saw how to visualize a model of the aircraft together with its current attitude. In this post, we'll see how to encapsulate the 3D graphics and helper functions used to visualize the aircraft in a custom chart.
Throughout this post, we'll use the term chart to refer to a MATLAB class that provides a convenient visualization API for end users. Charts are also more suitable for modular application development, since they can be dynamically updated after creation, and reused between multiple applications. For more information about charts, see the technical article [1].
By the end of this post, we'll have an aircraft chart equipped with the roll, pitch, and yaw functions for setting the attitude of the aircraft.
AircraftRoll.gif

Create a new chart class.

Since R2019b, MATLAB provides a framework for developing custom charts. We'll start by creating a new chart class within this framework, named AircraftChart.
classdef AircraftChart < matlab.graphics.chartcontainer.ChartContainer
end
In this example, the chart will only contain graphics objects. If you need your chart to contain control objects, such as buttons or check boxes, or if you need to share your chart as a reusable component in App Designer, you should use the ComponentContainer framework (introduced in R2020b). We’ll see some examples of components in part 3 of this blog series.

Define the setup and update methods.

If we try to create an AircraftChart object from our class, we'll get the following error.
>> AC = AircraftChart
Abstract classes cannot be instantiated. Class 'AircraftChart' inherits abstract methods or properties but does not implement
them. See the list of methods and properties that 'AircraftChart' must implement if you do not intend the class to be abstract.
This is because charts derived from ChartContainer must implement the following two methods:
  • setup - responsible for initializing the chart's graphics as part of the construction process.
  • update - responsible for refreshing the chart's graphics when the chart's data or properties are changed by an end user.
Let's add these methods to our AircraftChart. Note that we need to make these methods protected (i.e., accessible within the current class and any subclasses) for consistency with ChartContainer.
classdef AircraftChart < matlab.graphics.chartcontainer.ChartContainer
    methods ( Access = protected )
        function setup( obj )
        end
        function update( obj )
        end
    end
end
We can now create a chart object:
>> AC = AircraftChart
This is not very useful though - we don't yet have any data or graphics.

Add data to the chart.

As we saw in the first blog post, the data required for the visualization can be represented conveniently as a single triangulation object. Let's add this as a property in the AircraftChart class. As the default value, we'll use the triangulation obtained from the avion31 STL file we used previously.
classdef AircraftChart < matlab.graphics.chartcontainer.ChartContainer
    properties
        Triangulation(:, 3) triangulation = stlread( "avion31.stl" )
    end
    methods ( Access = protected )
        function setup( obj )
        end
        function update( obj )
        end
    end
end
The end user of the chart can easily change the aircraft STL file by setting the Triangulation property.
>> AC = AircraftChart
>> AC.Triangulation = stlread( "NewSTLFile.stl" );
The update method is called automatically when the end user sets any public property of the chart. This is the mechanism we'll use to ensure the chart updates dynamically in response to changes made by the end user. Before we do this, we need to add graphics objects to the chart.

Add graphics objects to the chart.

The ChartContainer framework provides a tiled layout in which to add the chart's graphics objects. We can access this tiled layout using the getLayout method. The tiled layout provides the flexibility to add multiple axes, or axes with different types such as geoaxes or polaraxes.
function setup( obj )
tl = obj.getLayout();
end
In this example, we need to initialize the following graphics objects:
  • An axes object, that we'll parent to the tiled layout.
  • A transform object created using hgtransform, that we'll parent to the axes. This is used to enable easy rotation of the aircraft around the three coordinate axes.
  • A patch object, that we'll parent to the transform object. Recall that we used the trisurf function to create the patch.
First, we'll add the graphics objects as chart properties.
classdef AircraftChart < matlab.graphics.chartcontainer.ChartContainer
    properties ( Access = private )
        Axes(:, 1) matlab.graphics.axis.Axes {mustBeScalarOrEmpty}
        Transform(:, 1) matlab.graphics.primitive.Transform {mustBeScalarOrEmpty}
        Patch(:, 1) matlab.graphics.primitive.Patch {mustBeScalarOrEmpty}
    end
end
There's no reason for an end user to retrieve or modify these objects directly, so we declare the attribute Access = private for these properties.
We also add property validation to ensure that these properties are used correctly within the class. Each of these properties should either be empty (when the class is first loaded) or a scalar value (when the setup method is called). Using the size validation (:, 1) combined with the validator mustBeScalarOrEmpty ensures that these properties either have size 0-by-1 or 1-by-1.
The type validation (e.g., matlab.graphics.axis.Axes) ensures that code within the class assigns values of the correct data type, e.g., to avoid confusion between the different types of axes. Conveniently, adding this type validation also enables tab-completion on properties of the graphics objects when these properties are referenced in methods.
Don't worry if these class names seem long - it's easy to discover the appropriate type to use by creating a graphics object at the command line and calling the class function.
>> class(axes)
ans =
    'matlab.graphics.axis.Axes'
Next, we'll create and assign the graphics objects in the chart's setup method. This is called automatically whenever a chart object is created. Here, we place the code we used in the first blog post for creating the axes, transform, and patch objects.
function setup( obj )
% Obtain the chart's tiled layout.
tl = obj.getLayout();
% Define the color scheme used for the aircraft.
blue = [0, 0.447, 0.741];
gray = 0.95 * ones(1, 3);
onesMatrix = ones(250, 3);
paintJob = [gray .* onesMatrix; blue .* onesMatrix];
% Create and customize the axes.
x = [-2000, 2000];
y = [-2500, 4500];
z = [-2500, 2500];
obj.Axes = axes( "Parent", tl, ...
    "Colormap", paintJob, ...
    "View", [225, 25], ...
    "DataAspectRatio", [1, 1, 1], ...
    "XLim", x, ...
    "YLim", y, ...
    "ZLim", z, ...
    "XTickLabel", [], ...
    "YTickLabel", [], ...
    "ZTickLabel", [], ...
    "NextPlot", "add");
% Add the transform and patch.
obj.Transform = hgtransform( "Parent", obj.Axes );
obj.Patch = trisurf( obj.Triangulation, ...
    "Parent", obj.Transform, ...
    "FaceAlpha", 0.9, ...
    "FaceColor", "interp", ...
    "EdgeAlpha", 0, ...
    "FaceLighting", "gouraud" );
% Add a title, lighting from above, and give the patch a shiny appearance by adjusting lighting properties.
title( obj.Axes, "Aircraft Attitude" )
light( obj.Axes, "Position", [0, 0, 1] )
material( obj.Axes, "shiny" )
end
Note that we do not include creation of the figure within the chart class. This is because a chart does not own or maintain a figure. Rather, it's best to think of a chart as a graphics object that may be placed in a figure, or any other appropriate container, e.g., a tab within a tab group, a panel, another tiled layout, or a grid layout. This is an important concept when using the chart in a modular application.

Customize the chart's constructor.

We'd like our chart to have a convenient, user-friendly API for end users to work with. To enable autocompletion on the input arguments accepted by the chart, we'll modify the default constructor of AircraftChart.
Chart constructors should accept name-value pair arguments, so we maintain this behavior using an arguments block, validating the name-value pairs from properties of the AircraftChart class.
We also set any name-value pairs on the chart that the end user has passed in to the constructor.
function obj = AircraftChart( namedArgs )
arguments ( Input )
    namedArgs.?AircraftChart
end
% Set any user-defined properties.
set( obj, namedArgs )
end

Update the chart's graphics.

The AircraftChart has one public property (Triangulation). When the end user sets this property, we expect the chart to update dynamically. This is the purpose of the update method. In our example, to avoid deleting and recreating the patch object, which could impair performance, we update the properties of the existing patch. This code uses the face-vertex representation of the patch object.
function update( obj )
faces = obj.Triangulation(:, :);
points = obj.Triangulation.Points;
set( obj.Patch, "Faces", faces, "Vertices", points, "FaceVertexCData", points(:, 3) )
end
In general, we can provide end users with a rich API comprising many properties for customizing the style and appearance of the chart. The update method handles changes made by the end user to these properties by refreshing the chart's private graphics properties.

Equip the chart with rotation methods.

Next, we'll transfer the roll, pitch, and yaw functions we discussed in the first blog post to the AircraftChart. These will become public methods of the chart, available for end users to call.
function roll( obj, theta )
obj.rotate( "roll", theta )
end % roll
function pitch( obj, theta )
obj.rotate( "pitch", theta )
end % pitch
function yaw( obj, theta )
obj.rotate( "yaw", theta )
end % yaw
On the other hand, the rotate helper function will become a private chart method, since an end user should not call this method directly.
function rotate( obj, type, theta )
arguments
    % Chart object.
    obj(1, 1) AircraftChart
    % Possible angles of rotation: roll, pitch and yaw.
    type(1, 1) string ...
        {mustBeMember( type, ["roll", "pitch", "yaw"] )}
    % Angular value to rotate by.
    theta(1, 1) double {mustBeReal, mustBeFinite} = 0
end % arguments
switch type
    case "roll"
        rotAxis = "y";
    case "pitch"
        rotAxis = "x";
    case "yaw"
        rotAxis = "z";
end % switch/case
% Convert the angle to radians and then update the
% transformation matrix.
theta = deg2rad( theta );
obj.Transform.Matrix = obj.Transform.Matrix * ...
    makehgtform( rotAxis + "rotate", theta );
end % rotate
It's good practice to include a method for restoring the original state of the chart. We'll do this by adding a reset method that sets the transformation matrix back to its original value of $I_4 $, the 4-by-4 identify matrix.
function reset( obj )
obj.Transform.Matrix = eye( 4 );
end % reset

Test the chart.

We're now ready to test the chart! Let's try out a 360-degree roll. Note that we’ve added a pause command to slow down the animation speed and ensure that each iteration of the loop is captured as a single frame in the resulting animation.
f = figure;
AC = AircraftChart( "Parent", f );
for k = 1 : 72
    AC.roll( 5 )
    pause( 0.05 )
end

Further charts.

Taking a similar approach, we could wrap up the graphics required for the map plot used on the dashboard into a custom chart named LocationChart. This chart encapsulates the following graphics objects:
  • A geographics axes (see geoaxes)
  • Line objects used for the flight path of the aircraft and the current position marker (see geoplot).
In the third part of this series, we’ll provide all the code for the components that make up the dashboard.

Summary

We've seen how to take our initial visualization code for the aircraft and encapsulate it into a reusable chart. The chart can be used by colleagues as a standalone visualization within a script or function, or integrated into a larger application. We've also introduced some best programming practices for developing a chart class in MATLAB.
In the next post, we'll see how to build up the dashboard by writing further modular components and integrating them into a MATLAB app.

References

 

|
  • print

评论

要发表评论,请点击 此处 登录到您的 MathWorks 帐户或创建一个新帐户。