//////////////////////////////////////////////////////////////// // // Copyright (c) 2007-2010 MetaGeek, LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // //////////////////////////////////////////////////////////////// // =================================================================== // This graph control was originally published by Stuart D. Konen // (skonen@gmail.com) as C2DPushGraph on December 2, 2006. // // The control has since been heavily modified, ripped apart, and put // back together for Inssider by Ryan Woodings at MetaGeek, LLC // (ryan@metageek.net). // =================================================================== using System; using System.Collections.Generic; using System.ComponentModel; using System.Drawing; using System.Drawing.Drawing2D; using System.Data; using System.Windows.Forms; using System.Text; using System.Diagnostics; namespace Inssider { public class TimeGraph : System.Windows.Forms.UserControl { // =================================================================== // PRIVATE POINT CLASS (Contains Line Data Members) // =================================================================== internal struct Point { internal float magnitude; internal bool measured; internal DateTime timeStamp; } // =================================================================== // INTERNAL LINE CLASS (Contains Line Data Members) // =================================================================== internal class Line { private TimeGraph _owner = null; private Color _color = Color.Green; private uint _numID = 0; private bool _selected = false; private bool _visible = true; internal int drawOrder = 0; internal DashStyle dashStyle = DashStyle.Solid; internal bool stripeDashed = false; internal List pointsList = new List(); internal Line(TimeGraph owner, uint id, Color color, bool dashed, bool striped) { _owner = owner; _numID = id; _color = color; drawOrder = (int)_numID; if (dashed) { dashStyle = DashStyle.DashDot; } stripeDashed = striped; } /// /// Clears any currently displayed magnitudes. /// internal void Clear() { pointsList.Clear(); _owner.Refresh(); } /// /// Gets the ID of this line /// internal uint ID { get { return _numID; } } /// /// Gets the timestamp of the last point in this line /// internal DateTime MaxTimeStamp { get { if (pointsList.Count > 0) { return pointsList[pointsList.Count - 1].timeStamp; } else { return DateTime.MinValue; } } } /// /// Gets the timestamp of the last point in this line /// internal DateTime MaxMeasuredTimeStamp { get { if (pointsList.Count > 0) { int i = pointsList.Count - 1; while ((i >= 0) && !pointsList[i].measured) { i--; } if (i >= 0) { return pointsList[i].timeStamp; } else { return DateTime.MinValue; } } else { return DateTime.MinValue; } } } /// /// Gets the timestamp of the first point in this line /// internal DateTime MinTimeStamp { get { if (pointsList.Count > 0) { return pointsList[0].timeStamp; } else { return DateTime.MinValue; } } } /// /// Gets or sets a value indicating whether the line is visible. /// internal bool Visible { get { return _visible; } set { if (_visible != value) { _visible = value; _owner.Invalidate(); } } } /// /// Sets or gets the line's current color. /// internal Color Color { set { if (_color != value) { _color = value; _owner.Refresh(); } } get { return _color; } } /// /// Sets or gets the line's thickness in pixels. NOTE: It is advisable /// to set HighQuality to false if using a thickness greater than /// 2 pixels as the antialiasing creates imperfections. /// internal bool Selected { set { if (_selected != value) { _selected = value; _owner.Refresh(); } } get { return _selected; } } /// /// Pushes a new magnitude (point) to the line with the passed /// numerical ID. /// /// /// The magnitude of the new point. /// /// /// The timestamp of the new point. /// /// /// Whether this point was measured, or estimated. /// internal void AddPoint(float magnitude, DateTime time, bool measured) { Point newPoint = new Point(); newPoint.magnitude = magnitude; newPoint.measured = measured; newPoint.timeStamp = time; if (FirstTimeStamp == DateTime.MinValue) { FirstTimeStamp = time; } if (newPoint.timeStamp > LatestTimeStamp) { LatestTimeStamp = newPoint.timeStamp; } pointsList.Add(newPoint); } internal void CropToTimes(DateTime minTime, DateTime staleTime) { // crop all data prior to minTime while ((pointsList.Count > 0) && (pointsList[0].timeStamp < minTime)) { pointsList.RemoveAt(0); } if (pointsList.Count > 0) { // if this line is stale update it's last point's timestamp to now if (!pointsList[pointsList.Count - 1].measured) { Point point = pointsList[pointsList.Count - 1]; point.timeStamp = DateTime.Now; pointsList[pointsList.Count - 1] = point; } // if the data just went stale create two points to mark the stale beginning and ending else if (pointsList[pointsList.Count - 1].timeStamp < staleTime) { AddPoint(-100, pointsList[pointsList.Count - 1].timeStamp.AddSeconds(1), false); AddPoint(-100, DateTime.Now, false); } } } } // =================================================================== // MAIN CONTROL CLASS // =================================================================== #region Private Data private Color _textColor = Color.Yellow; private Color _gridColor = Color.Green; private Color _graphBackColor = Color.Black; private bool _highQuality = true; private bool _autoScale = false; private bool _showLabels = true; private bool _showMinuteLines = true; private float _maxMagnitude = 100; private float _minMagnitude = 0; private int _gridSpacing = 20; private int _leftMargin = 0; private int _bottomMargin = 30; private int _rightMargin = 0; private int _topMargin = 0; private static TimeSpan DefaultTimeSpan = TimeSpan.FromMinutes(3); private TimeSpan _graphTimeSpan = DefaultTimeSpan; private float _pixelsPerMagnitude = 5; private Rectangle _clipRectangle = Rectangle.Empty; private bool _scanning = false; private SortedList _lines = new SortedList(); private System.ComponentModel.IContainer components = null; internal static DateTime LatestTimeStamp = DateTime.MinValue; internal static DateTime FirstTimeStamp = DateTime.MinValue; internal static DateTime MinTimeInView = DateTime.MinValue; internal static uint _lineWidth = 1; internal static uint _selectedWidth = 3; private const double MinPixelsPerMinute = 35; private const int MaxMinutesToStore = 60; private const int MaxSecondsBeforeStale = 10; #endregion #region Constructors public TimeGraph() { InitializeComponent(); InitializeStyles(); } public TimeGraph(Form Parent) { Parent.Controls.Add(this); InitializeComponent(); InitializeStyles(); } public TimeGraph(Form parent, Rectangle rectPos) { parent.Controls.Add(this); Location = rectPos.Location; Height = rectPos.Height; Width = rectPos.Width; InitializeComponent(); InitializeStyles(); } #endregion public void Start() { _scanning = true; } public void Stop() { _scanning = false; } /// /// Clean up any resources being used. /// /// true if managed resources should be disposed; otherwise, false. protected override void Dispose(bool disposing) { if (disposing && (components != null)) { components.Dispose(); } base.Dispose(disposing); } #region Component Designer generated code // =================================================================== /// /// Required method for Designer support - do not modify /// the contents of this method with the code editor. /// private void InitializeComponent() { components = new System.ComponentModel.Container(); } #endregion private void InitializeStyles() { BackColor = Color.Black; /* Enable double buffering and similiar techniques to * eliminate flicker */ SetStyle(ControlStyles.OptimizedDoubleBuffer, true); SetStyle(ControlStyles.UserPaint, true); SetStyle(ControlStyles.AllPaintingInWmPaint, true); DoubleBuffered = true; SetStyle(ControlStyles.ResizeRedraw, true); } #region Public Properties /// /// Gets or sets the color of any text displayed in the graph (labels). /// public Color TextColor { get { return _textColor; } set { if (_textColor != value) { _textColor = value; Refresh(); } } } /// /// Gets or sets the graph's grid color. /// public Color GridColor { get { return _gridColor; } set { if (_gridColor != value) { _gridColor = value; Refresh(); } } } /// /// Gets or sets the graph's grid color. /// public Color GraphColor { get { return _graphBackColor; } set { if (_graphBackColor != value) { _graphBackColor = value; Refresh(); } } } /// /// Gets or sets the top graph margin /// public int TopMargin { get { return _topMargin; } set { if (_topMargin != value) { _topMargin = value; adjustPixelsPerStep(); Refresh(); } } } /// /// Gets or sets the right graph margin /// public int RightMargin { get { return _rightMargin; } set { if (_rightMargin != value) { _rightMargin = value; Refresh(); } } } /// /// Gets or sets the right graph margin /// public int LeftMargin { get { return _leftMargin; } set { if (_leftMargin != value) { _leftMargin = value; Refresh(); } } } /// /// Gets or sets the right graph margin /// public int BottomMargin { get { return _bottomMargin; } set { if (_bottomMargin != value) { _bottomMargin = value; adjustPixelsPerStep(); Refresh(); } } } /// /// Gets or sets the number of grid lines to display. /// public int GridSpacing { get { return _gridSpacing; } set { if (_gridSpacing != value) { _gridSpacing = value; Refresh(); } } } /// /// Gets or sets the width (in pixels) of each graphed line /// public uint LineWidth { get { return _lineWidth; } set { _lineWidth = value; } } /// /// Gets or sets the width (in pixels) of each graphed line that is marked as "selected" /// public uint SelectedWidth { get { return _selectedWidth; } set { _selectedWidth = value; } } /// /// Gets or sets the maximum peek magnitude of the graph, which should be /// the largest value you could potentially push to the graph. NOTE: If you /// have set AutoScale to true, this value will automatically adjust to /// the highest magnitude pushed to the graph. /// public float MaxMagnitude { set { _maxMagnitude = value; } get { return _maxMagnitude; } } /// /// Gets or sets the minimum magnitude of the graph, which should be /// the smallest value you could potentially push to the graph. /// NOTE: If you have set AutoScale to true, this value will /// automatically adjust to the lowest magnitude pushed to the graph. /// public float MinMagnitude { set { _minMagnitude = value; } get { return _minMagnitude; } } /// /// Gets or sets the value indicating whether the graph automatically /// adjusts MinMagnitude and MaxMagnitude to the lowest and highest /// values pushed to the graph. /// public bool AutoScale { set { if (_autoScale != value) { _autoScale = value; Refresh(); } } get { return _autoScale; } } /// /// Gets or sets the value indicating whether the graph is rendered in /// 'high quality' mode (with antialiasing). It is suggested that this property /// be set to false if you intend to display your graph using bar graph /// styles, line thickness greater than two, or if maximum performance /// is absolutely crucial. /// public bool HighQuality { set { if (value != _highQuality) { _highQuality = value; Refresh(); // Force redraw } } get { return _highQuality; } } /// /// Gets or sets the value indicating whether the mimimum and maximum labels /// are displayed. /// public bool ShowLabels { set { if (_showLabels != value) { _showLabels = value; Refresh(); } } get { return _showLabels; } } /// /// Gets or sets the value indicating whether the minute lines are /// displayed on the graph /// public bool ShowMinuteLines { set { if (_showMinuteLines != value) { _showMinuteLines = value; Refresh(); } } get { return _showMinuteLines; } } /// /// Gets or sests the default amount of time to display on the graph /// public TimeSpan GraphTimeSpan { set { DefaultTimeSpan = value; } get { return DefaultTimeSpan; } } public Bitmap GraphBitMap { get { Bitmap bitmap = new Bitmap(this.Width, this.Height); Graphics graphics = Graphics.FromImage(bitmap); graphics.Clear(this.BackColor); DrawGraph(ref graphics); return bitmap; } } #endregion private void adjustPixelsPerStep() { _pixelsPerMagnitude = (this.Height - _topMargin - _bottomMargin) / (_maxMagnitude - _minMagnitude); } protected override void OnSizeChanged(EventArgs e) { _clipRectangle = new Rectangle(_leftMargin, _topMargin, Width - _leftMargin - _rightMargin, Height - _topMargin - _bottomMargin); adjustPixelsPerStep(); Refresh(); base.OnSizeChanged(e); } protected override void OnPaint(PaintEventArgs e) { Graphics g = e.Graphics;// SmoothingMode prevSmoothingMode = g.SmoothingMode; DrawGraph(ref g); g.SmoothingMode = prevSmoothingMode; } private void DrawGraph(ref Graphics g) { g.SmoothingMode = (_highQuality ? SmoothingMode.HighQuality : SmoothingMode.Default); DrawGrid(ref g); if (LatestTimeStamp <= DateTime.MinValue + DefaultTimeSpan) { return; } MinTimeInView = FirstTimeStamp; if (LatestTimeStamp - DefaultTimeSpan < FirstTimeStamp) { _graphTimeSpan = DefaultTimeSpan; } else { _graphTimeSpan = LatestTimeStamp - FirstTimeStamp; } double minutesInView = (double)(Width - _leftMargin - _rightMargin) / (double)MinPixelsPerMinute; if (_graphTimeSpan.TotalMinutes > minutesInView) { _graphTimeSpan = TimeSpan.FromMinutes(minutesInView); MinTimeInView = LatestTimeStamp - _graphTimeSpan; } if (_showMinuteLines) { DrawMinutes(ref g); } if (_clipRectangle != Rectangle.Empty) { g.SetClip(_clipRectangle, CombineMode.Replace); } DrawLines(ref g, false); DrawLines(ref g, true); g.ResetClip(); } private void DrawGrid(ref Graphics g) { Pen pen = new Pen(this.ForeColor); SolidBrush brush = new SolidBrush(_graphBackColor); String label; // fill graph background g.FillRectangle(brush, _leftMargin, _topMargin, Width - _leftMargin - _rightMargin, Height - _topMargin - _bottomMargin); // draw graph border g.DrawRectangle(pen, _leftMargin - 1, _topMargin, Width - _leftMargin - _rightMargin + 2, Height - _topMargin - _bottomMargin + 1); brush.Color = this.TextColor; //Draw Y-axis label SizeF labelSize = g.MeasureString(Localizer.GetString("AmplitudedBm"), Font); int y = (int)(((Height - _topMargin - _bottomMargin) / 2) + (labelSize.Width / 2) + _topMargin); System.Drawing.PointF rotationPoint = new PointF(8, y); System.Drawing.Drawing2D.Matrix matrix = new System.Drawing.Drawing2D.Matrix(); matrix.RotateAt(270, rotationPoint); g.Transform = matrix; g.DrawString(Localizer.GetString("AmplitudedBm"), this.Font, brush, 8, y); matrix.RotateAt(90, rotationPoint); g.Transform = matrix; // SJ - Added range to handle negative magnitudes float range = Math.Abs(_maxMagnitude - _minMagnitude); float step = _gridSpacing; float magnitude = _minMagnitude + _gridSpacing; while (magnitude < _maxMagnitude) { y = Height - _bottomMargin - (int)Math.Ceiling(step * _pixelsPerMagnitude); label = magnitude.ToString(); labelSize = g.MeasureString(label, Font); g.DrawString(label, Font, brush, _leftMargin - 5 - labelSize.Width, y - labelSize.Height / 2); pen.Color = _gridColor; pen.DashStyle = DashStyle.Dot; g.DrawLine(pen, _leftMargin, y, Width - _rightMargin, y); pen.Color = this.ForeColor; pen.DashStyle = DashStyle.Solid; g.DrawLine(pen, _leftMargin - 3, y, _leftMargin, y); magnitude += _gridSpacing; step += _gridSpacing; } pen.Dispose(); brush.Dispose(); } private void DrawMinutes(ref Graphics g) { Pen pen = new Pen(this.ForeColor); SolidBrush brush = new SolidBrush(this.TextColor); String timeString; SizeF stringSize; float gridSpacing = (float)(Width - _leftMargin - _rightMargin) / (float)_graphTimeSpan.TotalMinutes; float offset = _leftMargin + (60 - MinTimeInView.Second) * gridSpacing / 60; float y = Height - _bottomMargin; //bool firstDrawn = false; for (int n = 0; n < (int)_graphTimeSpan.TotalMinutes + 1; n++) { int x = (int)(offset + (n * gridSpacing)); if ((x > _leftMargin + 10) && (x < Width - _rightMargin - 10)) { g.DrawLine(pen, x, y, x, y + 3); timeString = MinTimeInView.AddMinutes(n + 1).ToShortTimeString(); //if (firstDrawn) { timeString = timeString.Replace(Localizer.GetString("PM"), ""); timeString = timeString.Replace(Localizer.GetString("AM"), ""); //} timeString = timeString.Trim(); stringSize = g.MeasureString(timeString, this.Font); g.DrawString(timeString, this.Font, brush, x - stringSize.Width / 2, y + ((_bottomMargin - stringSize.Height) / 2)); //firstDrawn = true; } } pen.Dispose(); brush.Dispose(); } private void DrawLines(ref Graphics g, bool selected) { Pen pen = new Pen(Color.White); Pen borderPen = new Pen(Color.FromArgb(122, _graphBackColor)); Pen backPen = new Pen(Color.FromArgb(242, 242, 242)); float yScaler = (float)(Height - _topMargin - _bottomMargin) / (_maxMagnitude - _minMagnitude); float xScaler = (float)(Width - _leftMargin - _rightMargin) / (float)_graphTimeSpan.TotalSeconds; // graphTimeSpan.TotalSeconds; DateTime minTimeToStore = DateTime.Now.AddMinutes(-MaxMinutesToStore); DateTime staleTime = DateTime.Now.AddSeconds(-MaxSecondsBeforeStale); int numPointsInView = 0; TimeSpan offset; GraphicsPath linePath; Line line; for (int i = 0; i < _lines.Count; i++) { //line = _lines.; // null; uint key = _lines.Keys[i]; line = _lines[key]; if ((line == null) || (!line.Visible) || (line.Selected != selected)) { continue; } if (_scanning) { line.CropToTimes(minTimeToStore, staleTime); } /* Now prepare to draw the line */ pen.Color = line.Color; if (line.Selected) { pen.Width = _selectedWidth; } else { pen.Width = _lineWidth; } backPen.Width = pen.Width; borderPen.Width = pen.Width + 2; pen.DashStyle = line.dashStyle; PointF[] pathPoints = new PointF[numPointsInView]; byte[] pointTypes = new byte[numPointsInView]; float x = 0; float y = 0; float prevX = _leftMargin; float prevY = Height - _bottomMargin; linePath = new GraphicsPath(); bool lineDrawn = false; bool measuredLine = true; try { for (int n = 0; n < line.pointsList.Count; n++) { if (line.pointsList[n].timeStamp >= MinTimeInView) { offset = line.pointsList[n].timeStamp - MinTimeInView; prevX = x; prevY = y; x = _leftMargin + ((float)offset.TotalSeconds * xScaler); y = Height - _bottomMargin - ((line.pointsList[n].magnitude + Math.Abs(_minMagnitude)) * yScaler); //y = Height - _bottomMargin - (line.pointsList[n].magnitude * yScaler); if (line.pointsList[n].measured == measuredLine) { linePath.AddLine(x, y, x, y); lineDrawn = false; } else { if (!line.pointsList[n].measured) { // draw the estimated line if (linePath.PointCount > 0) { g.DrawPath(borderPen, linePath); if (line.stripeDashed) { g.DrawPath(backPen, linePath); } g.DrawPath(pen, linePath); linePath = new GraphicsPath(); } pen.DashStyle = DashStyle.Dot; if (n > 0) { if (prevX != 0) { // if previous measured point is in view add that point linePath.AddLine(prevX, prevY, prevX, prevY); } else { Debug.Assert(true); } } linePath.AddLine(x, y, x, y); lineDrawn = false; } else { linePath.AddLine(x, y, x, y); g.DrawPath(borderPen, linePath); if (line.stripeDashed) { g.DrawPath(backPen, linePath); } g.DrawPath(pen, linePath); linePath = new GraphicsPath(); pen.DashStyle = line.dashStyle; linePath.AddLine(x, y, x, y); lineDrawn = false; } measuredLine = line.pointsList[n].measured; } } } if (!lineDrawn) { g.DrawPath(borderPen, linePath); if (line.stripeDashed) { g.DrawPath(backPen, linePath); } g.DrawPath(pen, linePath); } } catch (OverflowException) { /* ignore, it should be OK next time */ } } pen.Dispose(); backPen.Dispose(); borderPen.Dispose(); } /// /// Adds a new line using the passed numeric ID as an identifier and sets /// the line's initial color to the passed color. If successful, returns /// a handle to the new line. /// /// /// A unique numerical for the line you wish to create. /// /// /// The line's initial color. /// internal bool AddLine(uint numID, Color color, bool dashed, bool striped) { bool status = false; // check for conflicting ID if (!_lines.ContainsKey(numID)) { Line newLine = new Line(this, numID, color, dashed, striped); _lines.Add(numID, newLine); status = true; } return status; } /// /// Clears all data from the graph /// internal void Clear() { ClearLines(); ResetTimeValues(); } // End Clear() /// /// Removes all line datas from the graph /// internal void ClearLines() { if (null != _lines) { _lines.Clear(); Invalidate(); } }// End ClearLines /// /// Resets the internal time values. /// internal void ResetTimeValues() { _graphTimeSpan = DefaultTimeSpan; LatestTimeStamp = DateTime.MinValue; FirstTimeStamp = DateTime.MinValue; MinTimeInView = DateTime.MinValue; } // End ResetTimes() /// /// Removes a line by its numerical ID. /// /// /// The line's numerical ID. /// internal bool RemoveLine(uint numID) { return _lines.Remove(numID); } /// /// Returns a line with the specified id /// /// line id value /// a line object or null internal Line this[uint key] { get { Line line = null; if (_lines.ContainsKey(key)) { line = _lines[key]; } return line; } } } }