//----------------------------------------------------------------------- // // 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; } } }