//-----------------------------------------------------------------------
//
// Copyright © 2012 Nils Hammar. All rights reserved.
//
//-----------------------------------------------------------------------
/*
* Software to access vehicle information via the OBD-II connector.
*
* Copyright © 2012 Nils Hammar
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*
* Alternative licensing is possible, see the licensing document.
*
* The above text may not be removed or modified.
*/
namespace UserInterface.GUI.OBD
{
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Drawing.Imaging;
using System.Drawing.Printing;
using System.Globalization;
using System.IO;
using System.Windows.Forms;
using System.Windows.Forms.DataVisualization.Charting;
using global::Protocol.OBD;
using global::SharedObjects;
using global::SharedObjects.DataMgmt;
using global::SharedObjects.GUI;
using global::SharedObjects.Misc;
using global::SharedObjects.Protocol;
using global::SharedObjects.Protocol.OBD;
///
/// Panel for plot data.
///
public partial class PlotDataPanel : AbstractUserControl, IDataPresentation
{
///
/// Legend name label.
///
private const string LEGEND_NAME = "Legend";
///
/// Permitted characters when saving to file.
///
private const string ALLOWED_CHARS = " _()°%÷0123456789abcdefghijklmnopqrstuvwxyzåäöüøæñABCDEFGHIJKLMNOPQRSTUVWXYZÅÄÖÜØÆÑ";
///
/// Possible graphics file extensions.
///
private static readonly string[] extensions = { ".png", ".gif", ".bmp", ".jpg", ".tiff", ".emf", ".wmf", ".exif" };
///
/// Dictionary of graphs in plot.
///
private StackedDictionary graphs = new StackedDictionary();
//// private SortedDictionary graphs = new SortedDictionary();
///
/// Base timestamp to relate to.
///
private long t0;
///
/// Number of graphs.
///
private uint seriesNumber = 1;
///
/// Chart legend.
///
private Legend legend1;
///
/// Value span for primary Y axis.
///
/// This is used to determine if a secondary Y axis is needed or not when mixed values are presented.
///
///
private double span1 = -1;
///
/// Preferences instance.
///
private IPreferences iPreferences;
///
/// Chart title.
///
private string title;
///
/// Interval in milliseconds between samples.
///
private int pollInterval;
///
/// Initializes a new instance of the class.
///
public PlotDataPanel()
{
this.iPreferences = null;
this.title = null;
this.t0 = 0;
this.pollInterval = 0;
this.InitializeComponent();
}
///
/// Initializes a new instance of the class.
///
/// Event logging instance.
/// Preferences instance.
/// Application tree instance.
/// Graph title.
/// Base timestamp to relate to.
/// Interval in milliseconds between samples.
/// 'true' If the charts are grouped with more than one parameter per chart.
/// Mode byte value.
/// Pid value.
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1720:IdentifiersShouldNotContainTypeNames", MessageId = "byte", Justification = "Reviewed, intentional.")]
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Justification = "Reviewed, intentional.")]
public PlotDataPanel(
ILogging iLogging,
IPreferences iPreferences,
IApplicationTree applicationTree,
string title,
long t0,
int pollInterval,
bool grouped,
byte modeByte,
uint pidData)
: base(iLogging, null, applicationTree)
{
this.iPreferences = iPreferences;
this.title = title;
this.t0 = t0;
this.pollInterval = pollInterval;
this.InitializeComponent();
this.legend1 = new Legend();
this.legend1.Name = LEGEND_NAME;
this.addLegend(this.legend1);
if (grouped)
{
this.chart1.Titles.Add(title);
}
else
{
this.chart1.Titles.Add("[0x" + modeByte.ToString("x2") + ":0x" + pidData.ToString("x2") + "] " + title);
}
this.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom)
| System.Windows.Forms.AnchorStyles.Left)
| System.Windows.Forms.AnchorStyles.Right)));
/*
new CustomToolTip(this.printButton, "Print chart.");
new CustomToolTip(this.saveImageButton, "Save chart as image of size 1024x768.");
new CustomToolTip(this.saveCsvButton, "Save chart data to CSV file.");
*/
}
///
/// Restart plotting.
///
/// Base timestamp to relate to.
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Maintainability", "CA1500:VariableNamesShouldNotMatchFieldNames", MessageId = "t0", Justification = "Reviewed, intentional.")]
public void restart(long t0)
{
this.t0 = t0;
this.graphs.Clear();
}
///
/// Add data to the simple data table.
///
/// Presentation data wrapper.
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1062:Validate arguments of public methods", MessageId = "0", Justification = "Reviewed.")]
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Justification = "Reviewed.")]
public void simpleDataAdd(IPresentationData presentationData)
{
if (presentationData != null)
{
uint sensorId = 0;
double presentationMin = 0;
double presentationMax = 100;
string presentationUnit = "%";
if (presentationData.sensor != null)
{
sensorId = presentationData.sensor.id;
presentationMin = presentationData.sensor.presentation_min;
presentationMax = presentationData.sensor.presentation_max;
presentationUnit = presentationData.sensor.unit;
}
uint key = (uint)((uint)((presentationData.mode & 0xff) << 24) | (uint)((presentationData.pid & 0xffff) << 8) | (uint)(sensorId & 0xff));
uint[] keys = { presentationData.sourceAddress, presentationData.mode, presentationData.pid, sensorId };
Series series1 = this.graphs.Get(keys);
if (series1 == null)
{
series1 = new Series();
series1.ChartArea = "ChartArea1";
series1.ChartType = SeriesChartType.Line;
series1.Legend = this.legend1.Name;
series1.Name = "0x" + presentationData.sourceAddress.ToString("X2") + "; " + presentationData.label + ", #" + this.seriesNumber.ToString() + " (" + presentationData.unit + ")";
series1.MarkerStyle = System.Windows.Forms.DataVisualization.Charting.MarkerStyle.Square;
if (this.span1 == -1)
{
series1.YAxisType = AxisType.Primary;
this.span1 = Math.Abs(presentationMax - presentationMin);
// When we have the data type 'bit' the values are always in the range from 0 to 1.
if (presentationUnit == "bit"
&& this.chart1.ChartAreas.Count > 0)
{
ChartArea currentChartArea = this.chart1.ChartAreas[0];
currentChartArea.AxisY.Minimum = 0;
currentChartArea.AxisY.Maximum = 1;
}
}
else
{
double span2 = Math.Abs(presentationMax - presentationMin);
if ((Math.Abs(span2) > 0 && (this.span1 / span2) > this.iPreferences.secondaryYThreshold)
|| (Math.Abs(this.span1) > 0 && (span2 / this.span1) > this.iPreferences.secondaryYThreshold))
{
series1.YAxisType = AxisType.Secondary;
// When we have the data type 'bit' the values are always in the range from 0 to 1.
if (presentationUnit == "bit"
&& this.chart1.ChartAreas.Count > 0)
{
ChartArea currentChartArea = this.chart1.ChartAreas[0];
currentChartArea.AxisY2.Minimum = 0;
currentChartArea.AxisY2.Maximum = 1;
}
}
}
this.graphs.Add(keys, series1);
this.addChartSeries(series1);
this.seriesNumber++;
}
double x = (presentationData.t1 - this.t0) / 1000.0; // To get seconds on X-axis.
double y = presentationData.value;
this.addSeriesPoint(series1, x, y);
}
}
///
/// Add an item of the type Control.
///
/// ECU source address for data.
/// Which parameter mode that is selected.
/// Item to add.
public void add(uint sourceAddress, byte mode, Control control)
{
// Ignore.
}
///
/// View or hide action buttons on the graph.
///
/// 'true' if butttons shall be visible.
public void setViewButtonsVisible(bool visible)
{
this.printButton.Visible = visible;
this.saveImageButton.Visible = visible;
this.saveCsvButton.Visible = visible;
}
///
/// Save the chart image to the given path using default name.
///
/// Existing files with same name will NOT be overwritten.
/// If a file exists with the given name a new name will
/// be created using a counter value until a "free" name is created.
///
///
/// Path to save to.
public void saveChartImage(string path)
{
string fileName = path + "\\" + this.buildProposedFileName() + ".png";
int n = 0;
while (File.Exists(fileName))
{
fileName = path + "\\" + this.buildProposedFileName() + "." + n.ToString() + ".png";
}
this.saveChart(fileName);
}
///
/// Save the chart data to the given path using default name.
///
/// Notice that existing files with same name will be overwritten.
///
///
/// Path to save to.
public void saveData(string path)
{
string fileName = path + "\\" + this.buildProposedFileName() + ".csv";
int n = 0;
while (File.Exists(fileName))
{
fileName = path + "\\" + this.buildProposedFileName() + "." + n.ToString() + ".csv";
}
this.writeData(fileName);
}
///
/// Get the mapped data from the chart.
///
/// Number of data columns.
/// Mapped data dictionary.
/// Column labels.
public string[] getMappedData(int seriesColumns, SortedDictionary mappedData)
{
string[] labels = new string[seriesColumns];
if (mappedData != null)
{
int ix = 0;
foreach (Series series in this.chart1.Series)
{
if (series.Enabled)
{
labels[ix] = series.Name;
foreach (DataPoint point in series.Points)
{
int xPoint = (int)(point.XValue * 1000 / this.pollInterval);
DataPoint[] dpa;
if (mappedData.ContainsKey(xPoint))
{
dpa = mappedData[xPoint];
}
else
{
dpa = new DataPoint[seriesColumns];
mappedData.Add(xPoint, dpa);
}
dpa[ix] = point;
}
ix++;
}
}
}
return labels;
}
///
/// Count number of columns for out data.
///
/// Number of columns for out data.
public int countSeriesColumns()
{
int seriesColumns = 0;
foreach (Series series in this.chart1.Series)
{
if (series.Enabled)
{
seriesColumns++;
}
}
return seriesColumns;
}
///
/// Get the index number of the file extension so that the correct data format can be choosen.
///
/// If file type can't be identified this will default to .png.
///
///
/// Name of file including extension.
/// Index number for extension.
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Globalization", "CA1308:NormalizeStringsToUppercase", Justification = "Reviewed, intentional.")]
private static int getFileExtensionNumber(ref string fileName)
{
int extNum = 0;
for (extNum = 0; extNum < extensions.Length; extNum++)
{
if (fileName.ToLowerInvariant().EndsWith(extensions[extNum]))
{
break;
}
}
if (extNum == extensions.Length)
{
fileName += ".png";
extNum = 0;
}
return extNum;
}
///
/// Write the image to file.
///
/// Full path including extension to file to write to.
/// Image to write.
private static void writeToFile(string fileName, Bitmap bmp)
{
int extNum = getFileExtensionNumber(ref fileName);
if (!string.IsNullOrEmpty(fileName))
{
FileStream fs = null;
try
{
fs = new FileStream(fileName, FileMode.Create, FileAccess.Write);
switch (extNum)
{
case 0:
bmp.Save(fs, ImageFormat.Png);
break;
case 1:
bmp.Save(fs, ImageFormat.Gif);
break;
case 2:
bmp.Save(fs, ImageFormat.Bmp);
break;
case 3:
bmp.Save(fs, ImageFormat.Jpeg);
break;
case 4:
bmp.Save(fs, ImageFormat.Tiff);
break;
case 5:
bmp.Save(fs, ImageFormat.Emf);
break;
case 6:
bmp.Save(fs, ImageFormat.Wmf);
break;
case 7:
bmp.Save(fs, ImageFormat.Exif);
break;
default:
break;
}
}
catch (Exception ex)
{
MessageBox.Show(
ex.Message + "\r\n" + ex.StackTrace,
"Error",
MessageBoxButtons.OK,
MessageBoxIcon.Stop);
}
finally
{
if (fs != null)
{
try
{
fs.Close();
}
catch
{
}
}
}
}
}
///
/// Write the actual data.
///
/// Data to write.
/// Stream to write to.
/// Separator character for CSV file.
private static void writeFileData(SortedDictionary mappedData, StreamWriter streamWriter, string csvSep)
{
foreach (KeyValuePair kvp in mappedData)
{
DataPoint[] dpa = kvp.Value;
streamWriter.Write(dpa[0].XValue.ToString(CultureInfo.CurrentCulture));
foreach (DataPoint dp in dpa)
{
streamWriter.Write(csvSep);
if (dp != null)
{
streamWriter.Write(dp.YValues[0].ToString(CultureInfo.CurrentCulture));
}
}
streamWriter.WriteLine();
}
}
///
/// Write head row for CSV file.
///
/// Number of columns.
/// Labels to write.
/// Stream to write to.
/// Separator character for CSV file.
private static void writeHeadData(int seriesColumns, string[] labels, StreamWriter streamWriter, string csvSep)
{
streamWriter.Write("t (s)");
for (int i = 0; i < seriesColumns; i++)
{
streamWriter.Write(csvSep);
streamWriter.Write("\"");
streamWriter.Write(labels[i]);
streamWriter.Write("\"");
}
streamWriter.WriteLine();
}
///
/// Write the CSV file with data.
///
/// Full path of file to write.
/// Number of columns in data series.
/// Mapped data from chart.
/// Data labels.
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2202:Do not dispose objects multiple times", Justification = "Reviewed.")]
private static void writeFile(string fileName, int seriesColumns, SortedDictionary mappedData, string[] labels)
{
Stream fileStream = null;
StreamWriter streamWriter = null;
try
{
fileStream = new FileStream(fileName, FileMode.Create, FileAccess.Write);
streamWriter = new StreamWriter(fileStream, Utils.iso_8859_1);
string csvSep = ";";
if (CultureInfo.CurrentCulture.NumberFormat.NumberDecimalSeparator == ".")
{
csvSep = ",";
}
writeHeadData(seriesColumns, labels, streamWriter, csvSep);
writeFileData(mappedData, streamWriter, csvSep);
}
catch (Exception ex)
{
#if DEBUG
MessageBox.Show(
"Could not open file '" + fileName + "'\r\n\r\n" + ex.Message + "\r\n\r\n" + ex.StackTrace,
"Error",
MessageBoxButtons.OK,
MessageBoxIcon.Hand);
#else
MessageBox.Show(
"Could not open file '" + fileName + "'\r\n\r\n" + ex.Message,
"Error",
MessageBoxButtons.OK,
MessageBoxIcon.Hand);
#endif
}
finally
{
if (streamWriter != null)
{
try
{
streamWriter.Close();
}
catch
{
}
}
if (fileStream != null)
{
try
{
fileStream.Close();
}
catch
{
}
}
}
}
///
/// Add legend to chart.
///
/// Legend to add.
private delegate void addLegendFunc(Legend newLegend);
///
/// Add legend to chart.
///
/// Legend to add.
private void addLegend(Legend newLegend)
{
if (this.chart1.InvokeRequired)
{
try
{
this.Invoke(new addLegendFunc(this.addLegend), new object[] { newLegend });
}
catch (System.Reflection.TargetParameterCountException ex)
{
MessageBox.Show(
"Exception: " + ex.Message + "\r\n",
"Error",
MessageBoxButtons.OK,
MessageBoxIcon.Stop);
}
catch (System.ObjectDisposedException)
{
// Ignore.
}
}
else
{
this.chart1.Legends.Add(newLegend);
}
}
///
/// Add one point in the plot series.
///
/// Series to add point to.
/// X value for point.
/// Y value for point.
private delegate void addSeriesPointFunc(Series series1, double x, double y);
///
/// Add one point in the plot series.
///
/// Series to add point to.
/// X value for point.
/// Y value for point.
private void addSeriesPoint(Series series1, double x, double y)
{
if (this.chart1.InvokeRequired)
{
try
{
this.Invoke(new addSeriesPointFunc(this.addSeriesPoint), new object[] { series1, x, y });
}
catch (System.Reflection.TargetParameterCountException ex)
{
MessageBox.Show(
"Exception: " + ex.Message + "\r\n",
"Error",
MessageBoxButtons.OK,
MessageBoxIcon.Stop);
}
catch (System.ObjectDisposedException)
{
// Ignore.
}
}
else
{
series1.Points.AddXY(x, y);
}
}
///
/// Add series to chart.
///
/// Series to add.
private delegate void addChartSeriesFunc(Series series1);
///
/// Add series to chart.
///
/// Series to add.
private void addChartSeries(Series series1)
{
if (this.chart1.InvokeRequired)
{
try
{
this.Invoke(new addChartSeriesFunc(this.addChartSeries), new object[] { series1 });
}
catch (System.Reflection.TargetParameterCountException ex)
{
MessageBox.Show(
"Exception: " + ex.Message + "\r\n",
"Error",
MessageBoxButtons.OK,
MessageBoxIcon.Stop);
}
catch (System.ObjectDisposedException)
{
// Ignore.
}
}
else
{
this.chart1.Series.Add(series1);
}
}
///
/// Save chart as an image with size 1024x768.
///
/// Sending object.
/// Event data.
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Globalization", "CA1308:NormalizeStringsToUppercase", Justification = "Reviewed, intentional.")]
private void saveImageButton_Click(object sender, EventArgs e)
{
string lbl = this.buildProposedFileName();
this.saveFileDialog1.FileName = lbl;
this.saveFileDialog1.DefaultExt = ".png";
this.saveFileDialog1.AddExtension = true;
this.saveFileDialog1.ValidateNames = true;
this.saveFileDialog1.Filter = "Portable Network Graphics (png)|*.png|"
+ "Graphics Interchange Format (gif)|*.gif|"
+ "Bitmap Image File (bmp)|*.bmp|"
+ "Joint Photographic Experts Group (jpg, jpeg)|*.jpg;*.jpeg|"
+ "Tagged Image File Format (tif, tiff)|*.tif;*.tiff|"
+ "Enhanced Metafile (emf)|*.emf|"
+ "Windows Metafile (wmf)|*.wmf|"
+ "Exchangeable Image File Format (exif)|*.exif";
if (this.saveFileDialog1.ShowDialog() == DialogResult.OK)
{
string fileName = this.saveFileDialog1.FileName;
this.saveChart(fileName);
}
}
///
/// Save the current chart to file.
///
/// Name of file to save to.
private void saveChart(string fileName)
{
Bitmap bmp = null;
try
{
bmp = this.getBitmap();
writeToFile(fileName, bmp);
}
finally
{
if (bmp != null)
{
bmp.Dispose();
}
}
}
///
/// Get the chart as a bitmap image of size 1024x768.
///
/// Bitmap image.
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Justification = "Reviewed.")]
private Bitmap getBitmap()
{
Size savedSize = this.chart1.Size;
Size imageSize = new Size(1024, 768);
this.chart1.Size = imageSize;
Bitmap bmp = new Bitmap(imageSize.Width, imageSize.Height);
this.chart1.DrawToBitmap(bmp, new Rectangle(0, 0, imageSize.Width, imageSize.Height));
this.chart1.Size = savedSize;
return bmp;
}
///
/// Print chart.
///
/// Sending object.
/// Event data.
private void printButton_Click(object sender, EventArgs e)
{
PrintDialog MyPrintDialog = new PrintDialog();
try
{
if (MyPrintDialog.ShowDialog() == DialogResult.OK)
{
MyPrintDialog.Document = this.printDocument1;
this.printDocument1.PrintController = new System.Drawing.Printing.StandardPrintController();
this.printDocument1.PrintPage += this.printPage;
this.printDocument1.DefaultPageSettings.Landscape = true;
this.printDocument1.Print();
}
}
finally
{
MyPrintDialog.Dispose();
}
}
///
/// Print the graph.
///
/// Sending object.
/// Event data.
private void printPage(object sender, PrintPageEventArgs e)
{
// Get the size of the drawing area provided for printing.
Size drawSize = e.MarginBounds.Size;
// Save the current chart size so we can restore it.
Size savedSize = this.chart1.Size;
// Resize chart to the printable area size.
this.chart1.Size = drawSize;
// Generate a bitmap to contain the image.
Bitmap bmp = new Bitmap(drawSize.Width, drawSize.Height);
try
{
// Draw the chart to the bitmap.
this.chart1.DrawToBitmap(bmp, new Rectangle(0, 0, drawSize.Width, drawSize.Height));
// Restore the chart size for display in the GUI.
this.chart1.Size = savedSize;
// Draw the bitmap to the printer.
e.Graphics.DrawImage(bmp, e.MarginBounds);
}
finally
{
bmp.Dispose();
}
}
///
/// Save chart data to CSV file.
///
/// Sending object.
/// Event data.
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2202:Do not dispose objects multiple times", Justification = "Reviewed, intentional.")]
private void saveCsvButton_Click(object sender, EventArgs e)
{
string lbl = this.buildProposedFileName();
this.saveFileDialog2.FileName = lbl;
this.saveFileDialog2.DefaultExt = ".csv";
this.saveFileDialog2.AddExtension = true;
this.saveFileDialog2.ValidateNames = true;
this.saveFileDialog2.Filter = "Excel CSV Format|*.csv";
if (this.saveFileDialog2.ShowDialog() == DialogResult.OK)
{
string fileName = this.saveFileDialog2.FileName;
this.writeData(fileName);
}
}
///
/// Write CSV data to the file with the given name.
///
/// Full path and filename to write to.
private void writeData(string fileName)
{
int seriesColumns = this.countSeriesColumns();
SortedDictionary mappedData = new SortedDictionary();
string[] labels = this.getMappedData(seriesColumns, mappedData);
writeFile(fileName, seriesColumns, mappedData, labels);
}
///
/// Construct proposed name for file.
///
/// Proposed file name.
private string buildProposedFileName()
{
string lbl = string.Empty;
string intxt = this.title.Replace('/', '÷');
for (int i = 0; i < intxt.Length; i++)
{
if (ALLOWED_CHARS.IndexOf(intxt.Substring(i, 1)) >= 0)
{
lbl += intxt.Substring(i, 1);
}
}
return lbl;
}
}
}