/*
ScPl - A plotting library for .NET

LogAxis.cs
Copyright (C) 2003
Paolo Pierini, Matt Howlett

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions
are met:

1. Redistributions of source code must retain the above copyright
   notice, this list of conditions and the following disclaimer.
   
2. Redistributions in binary form must reproduce the following text in 
   the documentation and / or other materials provided with the 
   distribution: 
   
   "This product includes software developed as part of 
   the ScPl plotting library project available from: 
   http://www.netcontrols.org/scpl/" 

------------------------------------------------------------------------

THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

$Id: LogAxis.cs,v 1.19 2004/05/07 21:19:38 mhowlett Exp $

*/

using System;
using System.Drawing;
using System.Collections;
using System.Text;

namespace scpl
{
	/// <summary>
	/// The class implementing logarithmic axes.
	/// </summary>
	public class LogAxis : Axis
	{
		#region Clone implementation
		/// <summary>
		/// Deep Copy of the LogAxis.
		/// </summary>
		/// <returns>A Copy of the LogAxis Class.</returns>
		public override object Clone()
		{
			LogAxis a = new LogAxis();
			if (this.GetType() != a.GetType())
			{
				throw new System.Exception("Clone not defined in derived type. Help!");
			}
			this.DoClone( this, a );
			return a;
		}
		/// <summary>
		/// Helper method for Clone (actual implementation)
		/// </summary>
		/// <param name="a">The original object to clone.</param>
		/// <param name="b">The cloned object.</param>
		protected void DoClone(LogAxis b, LogAxis a)
		{
			Axis.DoClone(b,a);
			// add specific elemtents of the class for the deep copy of the object
			a.numberSmallTicks_ = b.numberSmallTicks_;
			a.largeTickValue_ = b.largeTickValue_;
			a.largeTickStep_ = b.largeTickStep_;
		}
		#endregion

		#region Constructors
		/// <summary>
		/// Default parameterless constructor.
		/// </summary>
		public LogAxis()
			: base()
		{
		}
		/// <summary>
		/// Constructor, cloning an existing Axis.
		/// </summary>
		/// <param name="a">The Axis to clone.</param>
		public LogAxis(Axis a)
			: base(a)
		{
		}

		/// <summary>
		/// Constructor that takes only world min and max values.
		/// </summary>
		/// <param name="worldMin">The minimum world coordinate.</param>
		/// <param name="worldMax">The maximum world coordinate.</param>
		public LogAxis(double worldMin, double worldMax)
			: base(worldMin,worldMax)
		{
		}
		#endregion

		#region DrawTicks
		/// <summary>
		/// Draw the ticks.
		/// </summary>
		/// <param name="g">The drawing surface on which to draw.</param>
		/// <param name="physicalMin">The minimum physical extent of the axis.</param>
		/// <param name="physicalMax">The maximum physical extent of the axis.</param>
		/// <returns> An ArrayList containing the offset from the axis required for an axis label
		/// to miss this tick, followed by a bounding rectangle for the tick and tickLabel drawn.</returns>
		protected override void DrawTicks(
			Graphics g, 
			PointF physicalMin, 
			PointF physicalMax, 
			out object labelOffset,
			out object boundingBox )
		{

			PointF tLabelOffset;
			RectangleF tBoundingBox;

			this.InitOffsetAndBounds( out labelOffset, out boundingBox );

			string strFormat = this.NumberFormat;

			// large ticks X positions are in l1
			// Log values are in the returned arraylist!
			ArrayList l1 = this.LargeTickPositions;

			PointF offset = new PointF( 0.0f, 0.0f );
			object bb = null;
			// Missed this protection
			if(l1.Count>0)
			{
				for (int i=0; i<l1.Count; ++i)
				{
					StringBuilder label = new StringBuilder();
					// do google search for "format specifier writeline" for help on this.
					label.AppendFormat(strFormat, (double)l1[i]);
					this.DrawTick( g, (double)l1[i], this.LargeTickSize, label.ToString(),
						new Point(0,0), physicalMin, physicalMax, out tLabelOffset, out tBoundingBox );

					this.UpdateOffsetAndBounds( ref labelOffset, ref boundingBox, tLabelOffset, tBoundingBox );
				}
			}
			else
			{
				// just get the axis bounding box)
				PointF dir=this.AxisNormVector(physicalMin,physicalMax);
				RectangleF rr=new RectangleF( physicalMin.X,
					(physicalMax.X-physicalMin.X)*dir.X,
					physicalMin.Y,
					(physicalMax.Y-physicalMin.Y)*dir.Y);
				bb=rr;
			}
			ArrayList stp = this.SmallTickPositions;

			// missed protection for zero ticks
			if(stp.Count>0)
			{
				for (int i=0; i<stp.Count; ++i)
				{
					this.DrawTick( g, (double)stp[i], this.SmallTickSize,
						"", new Point(0,0), physicalMin, physicalMax, out tLabelOffset, out tBoundingBox );
					// ignore r for now - assume bb unchanged by small tick bounds.
				}
			}

		}
		#endregion

		#region SmallTickPositions
		/// <summary>
		/// Fills and returns an ArrayList containing the small tick positions for the Log Axis.
		/// </summary>
		public override ArrayList SmallTickPositions
		{
			get
			{
				ArrayList toRet = new ArrayList();

				ArrayList l1 = this.LargeTickPositions;

				// retrieve the spacing of the big ticks. Remember this is decades!
				double bigTickSpacing = this.DetermineTickSpacing();
				int nSmall = this.DetermineNumberSmallTicks( bigTickSpacing );

				// now we have to set the ticks
				// let us start with the easy case where the major tick distance
				// is larger than a decade
				if (bigTickSpacing>1.0F)
				{
					if(l1.Count>0)
					{
						// deal with the smallticks preceding the
						// first big tick
						double pos1 = (double)l1[0];
						while (pos1> this.WorldMin)
						{
							pos1=pos1/10.0F;
							toRet.Add(pos1);
						}
						// now go on for all other Major ticks
						for (int i=0; i<l1.Count; ++i )
						{
							double pos=(double)l1[i];
							for (int j=1; j<=nSmall; ++j )
							{
								pos=pos*10.0F;
								// check to see if we are still in the range
								if (pos < WorldMax)
								{
									toRet.Add( pos );
								}
							}
						}
					}
				}
				else
				{
					// guess what...
					double [] m={2.0F,3.0F,4.0F,5.0F,6.0F,7.0F,8.0F,9.0F};
					// Then we deal with the other ticks
					if(l1.Count>0)
					{
						// first deal with the smallticks preceding the first big tick
						// positioning before the first tick
						double pos1=(double)l1[0]/10.0F;
						for (int i=0; i<m.Length; i++)
						{
							double pos=pos1*m[i];
							if (pos>this.WorldMin)
							{
								toRet.Add(pos);
							}
						}
						// now go on for all other Major ticks
						for (int i=0; i<l1.Count; ++i )
						{
							pos1=(double)l1[i];
							for (int j=0; j<m.Length; ++j )
							{
								double pos=pos1*m[j];
								// check to see if we are still in the range
								if (pos < WorldMax)
								{
									toRet.Add( pos );
								}
							}
						}
					}
					else
					{
						// probably a minor tick would anyway fall in the range
						// find the decade preceding the minimum
						double dec=Math.Floor(Math.Log10(WorldMin));
						double pos1=Math.Pow(10.0,dec);
						for (int i=0; i<m.Length; i++)
						{
							double pos=pos1*m[i];
							if (pos>this.WorldMin && pos< this.WorldMax )
							{
								toRet.Add(pos);
							}
						}
					}
				}
				return toRet;
			}
		}
		#endregion

		#region LargeTickPositions
		private static double m_d5Log = -Math.Log10(0.5);   // .30103
		private static double m_d5RegionPos = Math.Abs(m_d5Log + ((1 - m_d5Log) / 2)); //	   ' .6505
		private static double m_d5RegionNeg = Math.Abs(m_d5Log / 2); //	   '.1505
		private void CalcGrids( double dLenAxis, int nNumDivisions, ref double dDivisionInterval)
		{
			double dMyInterval  = dLenAxis / nNumDivisions;
			double dPower = Math.Log10(dMyInterval);
			dDivisionInterval = 10 ^ (int)dPower;
			double dFixPower = dPower - (int)dPower;
			double d5Region = Math.Abs(dPower - dFixPower);
			double dMyMult;
			if (dPower < 0)
			{
				d5Region = -(dPower - dFixPower);
				dMyMult = 0.5;
			}
			else
			{
				d5Region = 1 - (dPower - dFixPower);
				dMyMult = 5;
			}
			if ((d5Region >= m_d5RegionNeg) && (d5Region <= m_d5RegionPos))
			{
				dDivisionInterval = dDivisionInterval * dMyMult;
			}
		}

		/// <summary>
		/// Fills and returns an ArrayList containing the large tick positions for the Log Axis.
		/// </summary>
		public override ArrayList LargeTickPositions
		{
			get
			{
				if ( worldMin_ == null || worldMax_ == null )
				{
					throw new System.Exception( "world extent of axis not set." );
				}

				ArrayList res = new ArrayList();
				double roundTickDist = this.DetermineTickSpacing( );

				// now determine first tick position.
				double first = 0.0f;

				// if the user hasn't specified a large tick position.
				if (this.largeTickValue_ == null)
				{
					if( WorldMin > 0.0 )
					{

						double nToFirst = Math.Floor(Math.Log10(WorldMin) / roundTickDist)+1.0f;
						first = nToFirst * roundTickDist;
					}

					// could miss one, if first is just below zero.
					if (first-roundTickDist >= Math.Log10(WorldMin))
					{
						first -= roundTickDist;
					}
				}
					// the user has specified one place they would like a large tick placed.
				else
				{
					first = Math.Log10(this.LargeTickValue);

					// TODO: check here not too much different.
					// could result in long loop.
					while (first < Math.Log10(WorldMin))
					{
						first += roundTickDist;
					}

					while (first > Math.Log10(WorldMin)+roundTickDist)
					{
						first -= roundTickDist;
					}
				}

				double mark = first;
				while( mark <= Math.Log10(WorldMax))
				{
					// up to here only logs are dealt with, but I want to return
					// a real value in the arraylist
					double val;
					val=Math.Pow(10.0,mark);
					res.Add(val);
					mark += roundTickDist;
				}

				return res;
			}
		}
		#endregion

		#region DetermineTickSpacing
		/// <summary>
		/// Determines the tick spacing.
		/// </summary>
		/// <returns>The tick spacing (in decades!)</returns>
		private double DetermineTickSpacing( )
		{
			if ( worldMin_ == null || worldMax_ == null )
			{
				throw new System.Exception( "world extent of axis not set." );
			}

			// if largeTickStep has been set, it is used
			if (this.largeTickStep_!= null)
			{
				if ( (double)this.largeTickStep_ <= 0.0f )
				{
					throw new System.Exception( "can't have negative tick step - reverse WorldMin WorldMax instead." );
				}

				return (double)this.largeTickStep_;
			}

			double MagRange = (double)(Math.Floor(Math.Log10(WorldMax)) - Math.Floor(Math.Log10(WorldMin))+1.0);

			if ( MagRange > 0.0 )
			{
				// for now, a simple logic
				// start with a major tick every order of magnitude, and
				// increment if in order not to have more than 10 ticks in
				// the plot.
				double roundTickDist=1.0F;
				int nticks=(int)(MagRange/roundTickDist);
				while (nticks > 10)
				{
					roundTickDist++;
					nticks=(int)(MagRange/roundTickDist);
				}
				return roundTickDist;
			}
			else
			{
				return 0.0f;
			}
		}
		#endregion

		#region DetermineNumberSmallTicks
		/// <summary>
		/// Determines the number of small ticks between two large ticks.
		/// </summary>
		/// <param name="bigTickDist">The distance between two large ticks.</param>
		/// <returns>The number of small ticks.</returns>
		private int DetermineNumberSmallTicks( double bigTickDist )
		{
			// if the big ticks is more than one decade, the
			// small ticks are every decade, I don't let the user set it.
			if (this.numberSmallTicks_ != null && bigTickDist==1.0F)
			{
				return (int)this.numberSmallTicks_+1;
			}

			// if we are plotting every decade, we have to
			// put the log ticks. As a start, I put every
			// small tick (.2,.3,.4,.5,.6,.7,.8,.9)
			if( bigTickDist==1.0F)
			{
				return 8;
			}
				// easy, put a tick every missed decade
			else if (bigTickDist>1.0F)
			{
				return (int)bigTickDist-1;
			}
			else
			{
				throw new Exception("Wrong Major tick distance setting");
			}
		}
		#endregion

		// properties to box/unbox the private members
		// The LargeTickStep is in decades.
		// i.e. =1 for a tick every decades, =2 for a tick every 2 decades
		#region LargeTickStep
		/// <summary>
		/// The step between large ticks, expressed in decades for the Log scale.
		/// </summary>
		public double LargeTickStep
		{
			set
			{
				largeTickStep_ = value;
			}
			get
			{
				return (double)largeTickStep_;
			}
		}
		#endregion

		#region LargeTickValue
		/// <summary>
		/// Position for a large tick.
		/// </summary>
		public double LargeTickValue
		{
			set
			{
				largeTickValue_ = value;
			}
			get
			{
				return (double)largeTickValue_;
			}
		}
		#endregion

		#region NumberSmallTicks
		/// <summary>
		/// The number of small ticks between large ticks.
		/// </summary>
		public int NumberSmallTicks
		{
			set
			{
				numberSmallTicks_ = value;
			}
		}
		#endregion

		// Private members
		private object numberSmallTicks_;
		private object largeTickValue_;
		private object largeTickStep_;

		#region WorldToPhysical
		/// <summary>
		/// Transformation from world (user) coordinates to physical (screen) coordinates.
		/// </summary>
		/// <param name="coord">The coordinate value to transform</param>
		/// <param name="physicalMin"></param>
		/// <param name="physicalMax"></param>
		/// <param name="clip">Flag to indicate clipping.</param>
		/// <returns>The transformed coordinates.</returns>
		public override PointF WorldToPhysical( double coord, PointF physicalMin, PointF physicalMax, bool clip )
		{
			// if want clipped value, return extrema if outside range.
			if (clip)
			{
				if (coord > WorldMax)
				{
					return physicalMax;
				}
				if (coord < WorldMin)
				{
					return physicalMin;
				}
			}
			if (coord<0.0F)
			{
				throw new Exception("Cannot have negative values for data using Log Axis");
			}
			// inside range or don't want to clip.
			double lrange=(double)(Math.Log10(WorldMax) - Math.Log10(WorldMin));
			double prop = (double)((Math.Log10(coord) - Math.Log10(WorldMin)) / lrange);
			PointF offset = new PointF( (float)(prop * (physicalMax.X - physicalMin.X)),
				(float)(prop * (physicalMax.Y - physicalMin.Y)) );

			return new PointF( physicalMin.X + offset.X, physicalMin.Y + offset.Y );
		}

		#endregion

		#region PhysicalToWorld
		/// <summary>
		/// Transformation from physical (screen) coordinates to  world (user) coordinates.
		/// </summary>
		/// <param name="p">A physical coordinate.</param>
		/// <param name="physicalMin"></param>
		/// <param name="physicalMax"></param>
		/// <param name="clip">Flag to indicate clipping.</param>
		/// <returns>The transformed coordinates.</returns>
		public override double PhysicalToWorld( PointF p, PointF physicalMin, PointF physicalMax, bool clip )
		{
			// use the base method to do the projection on the axis.
			double t = base.PhysicalToWorld( p, physicalMin, physicalMax, clip );

			// now reconstruct phys dist prop along this assuming linear scale as base method did.
			double v = (t-this.WorldMin) / (this.WorldMax-this.WorldMin);

			double ret = WorldMin*Math.Pow(WorldMax/WorldMin,v);

			// if want clipped value, return extrema if outside range.
			if (clip)
			{
				ret=Math.Max(ret,WorldMin);
				ret=Math.Min(ret,WorldMax);
			}
			return ret;

		}
		#endregion

		#region WorldMin/WorldMax
		/// <summary>
		/// Accessor for the minimum extent of the world (user) coordinates.
		/// </summary>
		public new double WorldMin
		{
			get
			{
				return (double)base.WorldMin;
			}
			set
			{
				if (value>0.0F)
				{
					base.WorldMin = value;
				}
				else
				{
					throw new Exception("Cannot have negative values in Log Axis");
				}
			}
		}
		/// <summary>
		/// Accessor for the maximum extent of the world (user) coordinates.
		/// </summary>
		public new double WorldMax
		{
			get
			{
				return (double)base.WorldMax;
			}
			set
			{
				if (value>0.0F)
				{
					base.WorldMax = value;
				}
				else
				{
					throw new Exception("Cannot have negative values in Log Axis");
				}
			}
		}
		#endregion
	}
}
