/// /// This file is part of ILNumerics Community Edition. /// /// ILNumerics Community Edition - high performance computing for applications. /// Copyright (C) 2006 - 2012 Haymo Kutschbach, http://ilnumerics.net /// /// ILNumerics Community Edition is free software: you can redistribute it and/or modify /// it under the terms of the GNU General Public License version 3 as published by /// the Free Software Foundation. /// /// ILNumerics Community Edition 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 ILNumerics Community Edition. See the file License.txt in the root /// of your distribution package. If not, see . /// /// In addition this software uses the following components and/or licenses: /// /// ================================================================================= /// The Open Toolkit Library License /// /// Copyright (c) 2006 - 2009 the Open Toolkit library. /// /// Permission is hereby granted, free of charge, to any person obtaining a copy /// of this software and associated documentation files (the "Software"), to deal /// in the Software without restriction, including without limitation the rights to /// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of /// the Software, and to permit persons to whom the Software is furnished to do /// so, subject to the following conditions: /// /// The above copyright notice and this permission notice shall be included in all /// copies or substantial portions of the Software. /// /// ================================================================================= /// using System; using System.Collections.Generic; using System.Drawing; using ILNumerics.Drawing.Labeling; using System.Collections; namespace ILNumerics.Drawing.Collections { /// /// List of labeled ticks for an axis /// public class ILTickCollection : ILLabelingElement, IDisposable, IEnumerable { #region events /// /// Fires, when the collection of ticks has changed /// public new event AxisChangedEventHandler Changed; /// /// fires, if a new labeled tick is about to be added to the collection /// public event LabeledTickAddingHandler LabeledTickAdding; #endregion #region event handler /// /// fires the change event /// protected void OnChange() { if (Changed != null) Changed(this,new ILAxisChangedEventArgs(m_axisName)); } /// /// fires LabeledTickAdding event /// /// existing value /// existing label /// index of new tick in collection /// true: a registrar requested to cancel the adding, false otherwise protected bool OnLabeledTickAdding(ref float value, ref string label, int index) { if (LabeledTickAdding != null) { ILLabeledTickAddingArgs args = new ILLabeledTickAddingArgs(value,label,index); LabeledTickAdding(this,args); label = args.Expression; value = args.Value; return args.Cancel; } return false; } #endregion #region attributes private int m_ticksAllowOverlap = 10; private List m_ticks; private TickDisplay m_tickDisplay; private AxisNames m_axisName; private TickDirection m_tickDirection; private byte m_precision; private int m_padding; private Color m_tickColorNear; private Color m_tickColorFar; private TickMode m_tickMode; private float m_tickFraction; //internal properties internal Point m_lineStart; internal Point m_lineEnd; private TickLabelRenderingHint m_renderingHint; private ILPanel m_panel; #endregion #region properties /// /// determine the max number of pixels allowing a tic labels to be rendered inside the padding area of the next label /// public int TicksAllowOverlap { get { return m_ticksAllowOverlap; } set { m_ticksAllowOverlap = value; } } /// /// Get the prefered placement for tick labels or sets it /// /// Tick labels will not stricly rely on this setting, but rather /// try to find the optimal tick positions depending on the current /// rendering situation, taking the hint into account. public TickLabelRenderingHint RenderingHint { get { return m_renderingHint; } set { m_renderingHint = value; OnChange(); } } /// /// Positioning mode for ticks ([Auto],Manual) /// public TickMode Mode { get { return m_tickMode; } set { m_tickMode = value; OnChange(); } } /// /// get / set the color for near ticks (label side) /// public Color NearColor { get { return m_tickColorNear; } set { m_tickColorNear = value; OnChange(); } } /// /// get / set the color for backside ticks /// public Color FarColor { get { return m_tickColorFar; } set { m_tickColorFar = value; OnChange(); } } /// /// Get/ set the default color for tick labels /// /// The color may be overwritten for individual labels public Color LabelColor { get { return m_color; } set { m_color = value; OnChange(); } } /// /// padding between ticks /// public int Padding { get { return m_padding; } set { m_padding = value; OnChange(); } } /// /// number of digits to be displayed in axis ticks /// public byte Precision { get { return m_precision; } set { if (value < 16 && value > 0) m_precision = value; else throw new ILNumerics.Exceptions.ILArgumentException("precision must be in range 1..15!"); // caching: since tick labels will be redrawn every // render frame, we do not invalidate the label's queues here OnChange(); } } /// /// Get/ set which sides ticks for axis will be displayed on /// public TickDisplay Display { get { return m_tickDisplay; } set { m_tickDisplay = value; OnChange(); } } /// /// How ticks are displayed (inside/outside) /// public TickDirection Direction { get { return m_tickDirection; } set { m_tickDirection = value; OnChange(); } } /// /// length for ticks, fraction of the overall axis length. Default: 0.02 /// public float TickFraction { get { return m_tickFraction; } set { m_tickFraction = value; OnChange(); } } /// /// Number of ticks currently stored into the collection /// public int Count { get { return m_ticks.Count; } } /// /// The axis this tick collection is assigned to /// public ILAxis Axis { get { return m_panel.Axes[m_axisName]; } } #endregion #region constructor /// /// creates new ILTickCollection /// /// public ILTickCollection (ILPanel panel, AxisNames axisName) : base(panel,new Font(FontFamily.GenericMonospace, 10.0f),Color.Black) { m_panel = panel; m_ticks = new List(); m_axisName = axisName; m_precision = 4; m_padding = 5; m_tickColorFar = Color.Black; m_tickColorNear = Color.Black; m_tickDisplay = TickDisplay.LabelSide; m_tickMode = TickMode.Auto; m_tickFraction = 0.015f; m_renderingHint = TickLabelRenderingHint.Auto; } #endregion #region public methods /// /// Clear the collection of labeled ticks /// public void Clear() { m_ticks.Clear(); m_size = Size.Empty; } /// /// replace current collection of labeled ticks with a new one /// /// /// This will fire a Change event. [Obsolete] public void Replace(List ticks) { Set(ticks); } /// /// replace current collection of labeled ticks with a new one /// /// label position for every tick /// label texts for every tick corresponding to /// /// This will set the property to 'TickMode.Manual', hence preventing from /// automatic label creation on mouse perspective changes. /// Set creates a change event. /// public void Set(IEnumerable ticks, IEnumerable labels) { Clear(); IEnumerator tickCurs = ticks.GetEnumerator(); foreach (var lab in labels) { if (tickCurs.MoveNext()) { if (lab != null) Add(tickCurs.Current, lab.ToString()); else Add(tickCurs.Current); } else { throw new Exceptions.ILArgumentException("The number of 'labels' must match the number of tick locations."); } } this.Mode = TickMode.Manual; OnChange(); } /// /// replace current collection of labeled ticks with a new one /// /// label position for every tick /// /// This will set the property to 'TickMode.Manual', hence preventing from /// automatic label creation on mouse perspective changes. /// The Precision member is used to create tick label texts for all tick positions. /// Set creates a change event. /// public void Set(IEnumerable ticks) { Clear(); foreach (float tick in ticks) { Add(tick); } OnChange(); } /// /// Add a labeled tick to the ticks collection. /// /// current position /// current value /// This function will fire the LabeledTickAdding event. This /// gives users the chance to modify the tick and/or the label before /// it gets added. She can also cancel the adding for the tick at all.
/// No Change event for the axis will be fired from this method.
public void Add(float value, string label) { if (OnLabeledTickAdding(ref value, ref label, m_ticks.Count)) { return; } ILRenderQueue queue = m_interpreter.Transform(label, m_font,m_color,m_renderer); m_ticks.Add(new LabeledTick(value,queue)); if (m_size.Height < queue.Size.Height) m_size.Height = queue.Size.Height; if (m_size.Width < queue.Size.Width) m_size.Width = queue.Size.Width; //m_tickMode = TickMode.Manual; } /// /// Add a labeled tick to the ticks collection /// /// position for tick /// No Change event will be fired public void Add(float value) { this.Add(value,value.ToString("g"+m_precision)); } /// /// Get maximum size of /// all tick labels in pixels on screen /// /// This property does not take the orientation into account. The size /// of the content will be returned as if the orientation was straight horizontally. public override Size Size { get { // if (m_tickMode == TickMode.Auto && m_ticks.Count == 0) { // // first time: no labels have been added? -> assume maximum // // size determined by precision // Graphics g = Graphics.FromImage(new Bitmap(1,1)); // string measString = String.Format(".-e08" + new String('0',m_precision)); // m_size = g.MeasureString(measString,m_font).ToSize(); //} return m_size; } } ///// ///// (Re-)measures the maximum size for all labels contained in the collection ///// ///// graphics object, _may_ be used to measure texts (on some platforms) ///// maximum size ///// This function recomputes the true size for all labels and updates the internal cache. The cached value can ///// than queried by the property 'ScreenSize'. ///// Note: if no tick labels have been stored into the collection yet, the ///// maximum size possible for a label is returned. Therefore, the Precision member is ///// taken into account. //public Size MeasureMaxScreenSize(Graphics gr) { // if (m_ticks.Count == 0) { // SizeF sf = gr.MeasureString ( // "-e.08" + new String('0', m_precision),m_font); // m_screenSize = new Size((int)sf.Width,(int)sf.Height); // } else { // m_screenSize = new Size(); // Size tmp; // foreach (LabeledTick tick in this) { // tmp = tick.Queue.Size; // if (tmp.Height > m_screenSize.Height) // m_screenSize.Height = tmp.Height; // if (tmp.Width > m_screenSize.Width) // m_screenSize.Width = tmp.Width; // } // } // return m_screenSize; //} /// /// fill (replace) labels with nice labels for range /// /// lower limit /// upper limit /// maximum number of ticks public void CreateAuto(float min, float max, int tickCount) { float dist = max - min; tickCount = 50; string format = String.Format("g{0}",m_precision); // find the range for values we are dealing with. // how many ticks will (really) fit on the label line? if (tickCount > 1) { Replace(NiceLabels(min,max,tickCount,m_renderingHint)); if (Count > 0) return; // else: no ticks could be found at all -> fallback: show only center tickCount = 1; } Clear(); float relevExp = (float)Math.Round(Math.Log10((max - min))); if (!float.IsNaN(relevExp) && !float.IsInfinity(relevExp)) { float multRound; if (relevExp < 1) { multRound = (float) (1 / Math.Pow(10,relevExp-1)); } else { multRound = (float) Math.Pow(10,relevExp-1); } if (tickCount <= 1) { // If tickCount is 1, only the middle of Axis will be drawn. float ls = (float)(Math.Ceiling((max + min) / 2 / multRound) * multRound); if (ls != 0) Add(ls,ls.ToString(format)); else Add(min,min.ToString(format)); } else if (tickCount == 3) { // draw max, min and the approx. center of range Add(min,min.ToString(format)); Add(max,max.ToString(format)); float ls = (float)(Math.Round((max + min) / 2 / multRound) * multRound); Add(ls,ls.ToString(format)); } else { //if (tickCount == 2) { // If tickCount is less than 3 - only the max and the min will be drawn. Add(min,min.ToString(format)); Add(max,max.ToString(format)); } } else { Add(max,max.ToString(format)); } } /// /// Determine nice looking label positions for range specified /// /// lower limit /// upper limit /// maximum number of labels /// format string used to convert numbers to strings /// rendering hint, specifies preferred method /// list of tick labels public static List NiceLabels(float min, float max, int numMaxLabels, TickLabelRenderingHint hint) { List ret; switch (hint) { case TickLabelRenderingHint.Filled: ret = NiceLabelsFill(min,max,numMaxLabels); break; case TickLabelRenderingHint.Multiple1: ret = NiceLabelsEven(min,max,numMaxLabels,1.0f); break; case TickLabelRenderingHint.Multiple2: ret = NiceLabelsEven(min,max,numMaxLabels,2.0f); break; case TickLabelRenderingHint.Multiple5: ret = NiceLabelsEven(min,max,numMaxLabels,5.0f); break; default: //ret = NiceLabelsAuto(min,max,numMaxLabels,format); ret = loose_label(min,max,numMaxLabels); break; } return ret; } public void Draw(ILRenderProperties p, float min, float max) { m_renderer.Begin(p); float clipRange = max - min; ILPoint3Df mult = new ILPoint3Df( ((float)(m_lineEnd.X - m_lineStart.X) / clipRange), ((float)(m_lineEnd.Y - m_lineStart.Y) / clipRange), 0); float tmp; Point point = new Point(0,0); Point oldMidPoint = new Point(int.MinValue,int.MinValue); Point newMidPoint = new Point(int.MinValue,int.MinValue); Point oldHSize = new Point(), newHSize = new Point(); foreach (LabeledTick lt in m_ticks) { if (lt.Queue.Count > 0 && lt.Position >= min && lt.Position <= max) { tmp = lt.Position-min; point.X = (int)(m_lineStart.X + mult.X * tmp); point.Y = (int)(m_lineStart.Y + mult.Y * tmp); offsetAlignment(lt.Queue.Size, ref point); newHSize.X = (int)(lt.Queue.Size.Width / 2.0f); newHSize.Y = (int)(lt.Queue.Size.Height / 2.0f); newMidPoint.X = point.X + newHSize.X; newMidPoint.Y = point.Y + newHSize.Y; // check distance to last drawn label for tickmode auto (padding) if (m_tickMode == TickMode.Auto) { if (oldMidPoint.X != int.MinValue && oldMidPoint.Y != int.MinValue) { float distX = Math.Abs(newMidPoint.X - oldMidPoint.X); float distY = Math.Abs(newMidPoint.Y - oldMidPoint.Y); if (distX < (m_padding + oldHSize.X + newHSize.X) - m_ticksAllowOverlap && distY < (m_padding + oldHSize.Y + newHSize.Y) - m_ticksAllowOverlap) { //m_ticks.Remove(lt); continue; } } // bookmark outer rendering limits if (newMidPoint.X - newHSize.X < p.MinX) p.MinX = newMidPoint.X - newHSize.X; if (newMidPoint.Y - newHSize.Y < p.MinY) p.MinY = newMidPoint.Y - newHSize.Y; if (newMidPoint.X + newHSize.X > p.MaxX) p.MaxX = newMidPoint.X + newHSize.X; if (newMidPoint.Y + newHSize.Y > p.MaxY) p.MaxY = newMidPoint.Y + newHSize.Y; } m_renderer.Draw(lt.Queue, point, m_orientation, m_color); oldMidPoint = newMidPoint; oldHSize = newHSize; } } m_renderer.End(p); } #endregion #region IDisposable Member public new void Dispose() { if (m_renderer != null) { // ??? } } #endregion #region private helper /// /// Create nice labels, prefere even numbers over best optimal tick count /// /// min /// max /// max labels count /// format string used to convert numbers to strings /// nice label list private static List NiceLabelsAuto(float min, float max, int numMaxLabels, string format) { float[] divisors = new float[3] {5.0f, 2.0f, 1.0f}; List[] ticks = new List[divisors.Length]; int count = 0, bestMatch = int.MaxValue, tmp = 0, bestMatchIdx = -1; foreach (float div in divisors) { ticks[count] = NiceLabelsEven(min,max,numMaxLabels,div); if (ticks[count].Count == numMaxLabels) { return ticks[count]; } tmp = (int)Math.Abs(Math.Min(numMaxLabels,10) - ticks[count].Count); if (tmp < bestMatch) { bestMatch = tmp; bestMatchIdx = count; } count ++; } // its possible for limits to exceed the resolution for float // no ticks are returned in this case return ticks[bestMatchIdx]; } /// /// find tick labels by distinct divisors (10,5,2). Chooses the divisor which best produces the best number according to numberTicks /// /// minimum /// maximum /// maximum number of ticks /// format string for string conversion /// list of tick labels private static List NiceLabelsFill(float min, float max, int numberTicks) { // spacing may happen by divisors of 5,2,1. We test every case and choose the // one producing the best match on the number of ticks requested than. float[] divisors = new float[3] {5.0f, 2.0f, 1.0f}; List[] ticks = new List[divisors.Length]; int count = 0, bestMatch = int.MaxValue, tmp = 0, bestMatchIdx = -1; foreach (float div in divisors) { ticks[count] = NiceLabelsEven(min,max,numberTicks,div); tmp = (int)Math.Abs(Math.Min(numberTicks,10) - ticks[count].Count); if (ticks[count].Count == numberTicks) { return ticks[count]; } else if (tmp <= bestMatch) { bestMatch = tmp; bestMatchIdx = count; } count ++; } System.Diagnostics.Debug.Assert(bestMatchIdx >= 0,"No best divisor has been found. Tick creation failed."); if (bestMatchIdx >= 0) return ticks[bestMatchIdx]; else return ticks[0]; // rescue plan } private static List NiceLabelsEven(float min, float max, int numberTicks, float divisor) { //minimal distance required for ticks float minDist = (max - min) / (numberTicks + 1); // determine prominent range for optimal spacing if (Math.Abs(minDist) < 1e-10) return new List(); float relevExp = (float)Math.Log10((max - min)); if (float.IsNaN(relevExp)) { return new List(); } float multRound; if (relevExp >= 1) { return NiceLabelsEvenGE10(min,max,numberTicks,divisor); } List ticks = new List(); relevExp = (float)Math.Round(relevExp); multRound = (float) (1 / Math.Pow(10,relevExp-1)); //float step = divisor / multRound, cur = min, origStep = step; float step = multRound/ divisor, cur = min, origStep = step; // TODO: check for step beeing too small for resonable min..max distance! // OR: prevent for infinite loop! while (step < minDist) { step += origStep; } // find the first auto tick value, matching the divisor cur = (float)(Math.Round((min + minDist) * multRound / divisor) / multRound * divisor); ticks.Add(cur); cur += step; // add all ticks in _between_, keep margin to axis limits! while (cur < max) { cur = (float)(Math.Round(cur * multRound) / multRound); ticks.Add(cur); cur += step; if (ticks.Count > numberTicks) { // emergency break - float cannot handle this distance! ticks.Clear(); break; } } return ticks; } private static List NiceLabelsEvenGE10(float min, float max, int numberTicks, float divisor) { //minimal distance required for ticks float minDist = (max - min) / (numberTicks + 1); // determine prominent range for optimal spacing List ticks = new List(); // has been checked, if comming from NiceLabelsEven: //if (Math.Abs(minDist) < 1e-10) return ticks; float relevExp = (float)Math.Log10((max - min)); if (float.IsNaN(relevExp)) { return ticks; } float multRound; relevExp = (float) (Math.Sign(relevExp) * Math.Floor(Math.Abs(relevExp))); multRound = (float) Math.Pow(10,relevExp-1); //float step = divisor / multRound, cur = min, origStep = step; float step = multRound/ divisor, cur = min, origStep = step; // TODO: check for step beeing too small for resonable min..max distance! // OR: prevent for infinite loop! while (step < minDist) { // increase the multRRound by factor 10 relevExp ++; multRound = (float) (Math.Pow(10,relevExp-1)); step = multRound/ divisor; } // find the first auto tick value, matching the divisor cur = (float)niceNumber(min,false); ticks.Add(cur); // add all ticks in _between_, keep margin to axis limits! while (cur < max) { cur = (float)niceNumber(cur + step,false); ticks.Add(cur); if (ticks.Count > numberTicks) { // emergency break - float cannot handle this distance! ticks.Clear(); break; } } return ticks; } /// /// create "nice" number in fractions of 2 or 5 /// /// value /// This code was adopted from Paul Heckbert /// from "Graphics Gems", Academic Press, 1990. private static double niceNumber(double val, bool round) { int expv; double f; /* fractional part of x */ double nf; /* nice, rounded fraction */ double aval = Math.Abs(val); double sign = Math.Sign(val); expv = (int)Math.Floor(Math.Log10(aval)); f = aval/Math.Pow(10, expv); /* between 1 and 10 */ if (round) { if (f<1.5) nf = 1; else if (f<3) nf = 2; else if (f<7) nf = 5; else nf = 10; } else { if (f<=1) nf = 1; else if (f<=2) nf = 2; else if (f<=5) nf = 5; else nf = 10; } return (nf * Math.Pow(10, expv) * sign); } private static List loose_label(float min,float max, int numberTicks) { double d; /* tick mark spacing */ double graphmin; /* graph range min and max */ double range, x; /* we expect min!=max */ range = niceNumber(max-min, false); d = niceNumber(range/(Math.Min(numberTicks,10)-1), true); double exp = Math.Pow(10,Math.Floor(Math.Log10(max - min))); graphmin = Math.Floor(min / exp) * exp; // Math.Min(niceNumber(min - d,true),niceNumber(min - d,false)); //graphmax = Math.Min(niceNumber(max - d, true), niceNumber(max + d, false)); List ticks = new List(); //ticks.Add(nfrac); for (x = graphmin; x <= max; x = (float)(Math.Round((x + d) / d) * d)) { //x = (float)(Math.Round(x/d)*d); ticks.Add((float)x); if (ticks.Count > 20) break; //emergency exit if range is in floating point range } return ticks; } #region Oli's labels implementaion (incomplete) private static List niceLabels (float start, float end, int maxNumbers,string format) { float[] labelPositions = berechneLabel(start,end,maxNumbers); List ret = new List(); foreach (float pos in labelPositions) { ret.Add(pos); } return ret; } private static float[] berechneLabel(float start, float end, int maxNumbers) { if (end <= start) return null; float strecke = Math.Abs(end - start); maxNumbers++;//�berpr�fen! int n = 0; while (strecke < maxNumbers) { strecke *= 10; n++; } while (strecke > 100) { strecke /= 10; n--; } start *= (float)Math.Pow(10, n); int streckeIntValue = getStreckeAsNiceInt(strecke, maxNumbers); /* if (isPrimzahl(streckeIntValue) && streckeIntValue != maxNumbers) { streckeIntValue++; //Kann zu Problemen f�hren! -- ist aber auch Mist. } */ float step = 0; int stepCount; for (stepCount = maxNumbers; stepCount > 0; stepCount--) { step = streckeIntValue / stepCount; int rest = streckeIntValue % stepCount; if(rest < step && (step == 1 || step == 2 || step == 5 || step == 10 || step == 25 || step == 50 )) break; } int startInt = (int)Math.Round(start, 0); startInt = startInt - (startInt % (int)step); if (n != 0) { step /= (float)(Math.Pow(10, n)); start = (float)startInt / (float)Math.Pow(10, n); } else start = startInt; if (!(start + stepCount * step < end)) stepCount--; float[] ret = new float[stepCount]; for (int i = 1; i <= stepCount; i++) { ret[i - 1] = start + i * step; } return ret; } private static int getStreckeAsNiceInt(float strecke, int maxNumbers) { int streckeIntValue = (int)Math.Round(strecke, 0); if (streckeIntValue <= 10) return streckeIntValue; if (streckeIntValue / 2 <= maxNumbers) return streckeIntValue; streckeIntValue = streckeIntValue - streckeIntValue % 5; return streckeIntValue; } #endregion #endregion #region IEnumerable Member /// /// Get Enumerator, enumerating over all labeled ticks /// /// Enumerator public IEnumerator GetEnumerator() { return m_ticks.GetEnumerator(); } #endregion #region IEnumerable Member /// /// Get enumerator enumerating over labeled ticks /// /// enumerator System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() { foreach (LabeledTick lt in m_ticks) yield return lt; } #endregion } }