Introduction
About a month ago, I was using the MoTeC i2 Data Analysis software to look through some telemetry data that I had exported from a popular racing simulator game. Although MoTeC is feature rich and a great tool for motor racing professionals and enthusiasts, I had the idea to create a simplified, styled version using Silverlight. I had been looking for an interesting idea to test out my newfound Silverlight skills for a while, and this seemed like the perfect opportunity.
Before reading the rest of the article, you may like to view the sample application, located at the bottom of this post.
Using the Code
Architecture
The application as it is presented here is not the first version that I created. I had worked on an initial version before this which did not use the MVVM pattern. As a result of this, making changes were difficult, duplicate strings representing the same series name were littered throughout the application, the code behind was duplicated for each of the three chart components, and performance was far less than optimal. In my defense, the application I first envisioned was a single, simple chart with one series and no further information. But when I started to expand the application, all of these issues arose and started to get progressively worse.
So I decided to start again, switching to a cleaner, more manageable architecture that made use of data binding and limited code behind. The code is now structured using the MVVM architectural pattern . This should hopefully be clear by examining the solution and source code provided alongside the article.
Model
When creating the model, the aim was for it to be as reusable as possible. Originally I had dependencies on the Visiblox Charting Component (see the Charts section). However, these were removed, in favour of a model with simple lists of values. It would be the responsibility of the ViewModel to convert these values into the appropriate format before they were displayed by the charts in the view.
The model in the solution provided is in the TelemetryData
Project. This project reads and parses the .csv values exported from MoTeC. TelemetryDataProvider
implements ITelemetryDataProvider
and creates and populates a list of TelemetrySeries
in its constructor. One of the main goals when creating the TelemetryDataProvider
was for it to be as generic as possible. Users of the model can therefore obtain a list of string
s representing the names of all the series by reading the SeriesList
property. This means the string
representations of the series names do not need to be replicated anywhere else in the code. Users can also obtain a particular telemetry series by calling the FindSeriesFromName
method and passing a string
representation of the TelemetrySeries
.
A TelemetrySeries
represents a series of TimestampedDataPoints
along with the Name of the series and the index of the column in the csv file that the series data is stored in.
The next challenge was how to populate the SeriesList
in TelemetryDataProvider
. In the ReadFile(StreamReader)
method, the provider creates a TelemetrySeriesDictionaryPopulator
, passing its constructor the location of the .csv file and the SeriesList
. For every valid line in the .csv file, the TelemetrySeriesDictionaryPopulator
will call its FillFromCSV
method, which will add the relevant values to each AllSeries
entry, based on the entry's IndexInCSV
value.
One of my favourite things about the way TelemetryDataProvider
is implemented is how easy it is to add new Series data to the application. To add a TelemetrySeries
to the application, only one line of code is required:
This will add a series to the application, and no other references in the project need to be updated for the series to be visible. The FillFromCSV
method mentioned earlier will also need no modifications. I chose to display the 10 data sets which I saw to be most appropriate, but this is easily configurable through TelemetryDataProvider
. There are 115 columns to choose from in the exported MoTeC file.
View
The View presents the data provided by the View Model. Both the View and View Model are located in the TelemetryPrototype
project. There is very little code behind in the .xaml view files. The only code that is present is strictly necessary and could not be easily achieved through XAML. TelemetryChannelCollectionView
for example, rebuilds the ChannelView
grid depending on how many ChannelView
s are currently being displayed. Through reducing the code behind, I was able to ensure that the View
and the ViewModel
became almost independent of each other. This allows other Views to be attached to the same ViewModel in future, without any loss of functionality.
The views are attached to the view model through the use of .xaml bindings, which update automatically when the respective property in the ViewModel changes.The TelemetryView
, TelemetryChannelCollectionView
and TelemetryChannelView
have their Data Contexts set to their ViewModels upon initialisation, which are described in the section below.
ViewModel
I wanted all of the logic of the application to be contained in the ViewModel and not inside of the Model or the View. The ViewModel therefore abstracts from the View and provides a binding between the View and the Model. Any commands from the View are simply passed to the ViewModel via an ICommand
object, which will cause some property on the ViewModel to update. Through bindings, this will then update the view accordingly.
TelemetryViewModel
provides the top level data context, and contains a TelemetryChannelCollectionViewModel
which stores and displays the ChannelViewModels
currently added to the application. It also holds properties for the custom controls in the bottom panel, which are updated in ViewModelBase
as well as Commands for playback speed, restart, full data, play/pause, import data and add chart.
Another of its main responsibilities is to update the charts during live playback of the data. The UpdateChannelViewModels
method is called by a dispatcher timer every 50ms.It calculates the time elapsed since the last update, and then calls updateChart
on the ChannelViewModel
s in the ChannelViewModel
collection. The widgets are also updated based on the counter. If the chart is paused, the widgets are updated based on the selected index of the top chart (which should be synchronised with the other charts in the application).
The TelemetryChannelViewModel
contains the properties and values required to display a chart. It provides the data context for a TelemetryChannelView
and contains two data series to display on the chart from that view. It also contains behaviours and operations to modify, remove and add data series to the chart.
Charts
Custom Zoom Behaviour
To add the charts to the application, I used Visiblox Silverlight Charts. I chose to use Visiblox as I was looking for a high performance charting component. I knew I would be plotting a lot of data on the charts and the performance comparisons on Colin Eberhardt's blog here, as well as opinions on Stack Overflow led me to select Visiblox. I found the examples to be a great help when setting up the charts for the application.
Sticking to the MVVM architecture for the charts, the data for the charts comes from the Model, is converted by the ViewModel and displayed by the View, using the following method. Each chart is populated by its TelemetryChannelViewModel
, which obtains data for the two series from the GetChannelData
method in TelemetryViewModel
. This method retrieves the relevant TelemetrySeries
from the Data Provider in the Model, and calls TelemetryDataToVisibloxDataSeries
, which adds each point in the TelemetrySeries
to a Visiblox DataSeries
type. The Visiblox LineSeries
' DataSeries
in TelemetryChannelView
then has a binding to the TelemetryChannelViewModel
's LivePrimaryChartDataSeries
/LiveSecondaryChartDataSeries
property.
To keep the data series on each of the charts in context, I decided it would be a good idea to restrict the zoom behaviours to the X-Axis. There is no way to restrict the standard Visiblox ZoomBehaviour
to the X-Axis, so I had to define a new one. XAxisZoomBehaviour
implements the Visiblox BehaviourBase
abstract Class. The MouseLeftButtonDown
method sets a zoom rectangle to the height of the chart, and when the MouseMove
method is called, the width of this rectangle is modified to the value of the mouse position minus the start position. The method also checks if the rectangle covers a large enough area on the chart to allow a zoom. By checking this, it stops the zoom area from becoming too small.If the mouse is released and the zoom area is large enough, the ZoomTo
method is called.
This code from ZoomTo
creates two double animations, one for the Scale, and one for the Offset of the zoom. This is the code which actually modifies the offset and scale of the chart when the storyboard sb
begins.
Controls
When the charts were added to the application, I still felt like it was missing something. I had the idea to add widget-style custom controls below the chart to add more interest and interactivity. Each of the controls is defined in its own class in the TelemetryPrototype.controls
namespace within the controls folder. They all have at least one dependency property and subscribe to onPropertyChanged
events, which allows the controls to automatically update whenever one of their dependency properties is modified. The layout and elements of all the controls are defined in the Generic.xaml file.
ThrottleBrakeControl.cs
Dependency Properties:Throttle, Brake
The throttle Control is a three column grid, with a throttle indicator on the left, a brake indicator on the right and labels in the centre. The indicators are created by displaying a rectangle inside of a border. The rectangles are styled appropriately (green for throttle and red for brake) and their height is bound to their respective dependency property.
CarLocationControl.cs
Dependency Properties:Position
The car location control shows where the car currently is on the track.The track itself was created in Microsoft Expression Blend 4 using a path. The Microsoft.Expression.Controls
and Microsoft.Expression.Drawing
.dll files are therefore included in the references of this project. A PathListBox
then binds its LayoutPath SourceElement
to this path. The car itself is simply an ellipse inside of this PathListBox
. By binding the Start
property of the LayoutPath
to the Position
dependency property, the ellipse will move around the path. Position
must therefore be a percentage value. To calculate the percentage completion of a lap, it is necessary to have the start and end distance, which are calculated in the TelemetryDataProvider.cs when the data is first read into the application.
GForceControl.cs
Dependency Properties:Lateral, Long
There is slightly more code for the G-Force control and no bindings for the values on the chart, as these are set manually whenever a property changes in the OnLateralPropertyChanged
and OnLongPropertyChanged
methods. Within the OnApplyTemplate()
method of the control, a DataPoint
is created and added to a line series. This line series is then added to the chart.
The G-Force chart has also been retemplated to remove the border and to hide any axis values. The template is contained in App.xaml, with a target type of Chart. It has a key value of Gchart, which is referenced in the control's layout in the Generic.xaml by "Template="{StaticResource Gchart}"
.
SpeedometerControl.cs
The speedometer control is a modified version of the gauge control developed by Colin Eberhardt as part of a blog post entitled "Developing a (very) Lookless Silverlight Radial Gauge Control". The only modifications from the original source code from the article are the styles, layout and namespace declarations. The style is defined at the bottom of the generic.xaml file in the Themes folder of the SilverTrack
project.
Util/Other Classes
Data Context Changed Handler
In order to register change events on the Data Context within the Telemetry Application, Jeremy Likeness' DataContextChangedHelper
class was used.
App.xaml
The styles, brushes, and custom templates for the charts are all defined in the App.xaml file.
Generic.xaml
The layout for the controls and the templates are defined in the Generic.xaml file.
Points of Interest
This was the first time I had used the MVVM architecture, and I will certainly use it again in the future. I had developed a prototype version of this application without MVVM. I had planned on building on this to create the final version, but found that it was much harder to modify and add additional functionality to. Mixing UI code with Logic code is bad practice, and it is something I will now be keen to avoid in the future.
It was also the first time I had used a charting component in Silverlight, other than the toolkit version. I decided on Visiblox after reading this article from Colin Eberhardt and others like it, as well as reading charting recommendations from members of Stack Overflow.
Exporting Data From MoTeC
Using the import data button in the top left of the application, it is possible to load your own MoTeC data into SilverTrack. If you do not wish to use the sample files provided, and have your own way to import telemetry data to MoTeC, exporting to the .csv format used by the application is relatively straightforward:
- Open a Log File, then select Export Data...
- Set current range to a single lap, and output format as CSV.
- Performance is optimal with sample rate of 20Hz.
- Include Time Stamp and Distance Data.
NOTE: The car location control will only operate correctly if you load data from the Calder Road Course.