Silverlight数学引擎(4)——X之谜

在表达式中加入变量有什么用呢?看看下图就知道了:

含有x的表达式无法直接求值,需要先对x进行赋值,例如对于(y=x*x)不断的赋值、取值,可以绘制出一一列点(x,y),从而可以绘制出平滑的抛物线。

所以,XNode节点必须有个可赋值的属性:XValue,而且应该是静态的,以下是XNode的定义(超级简单,是不是?):

    public class XNode : NodeBase
    {
        public static double XValue { get; set; }
        public XNode(int index, string data, string expression)
            : base(index, data, expression)
        {
        }

        public override double GetValue()
        {
            return XValue;
        }
    }

变量、常量的节点只要一次性解析完毕,以后的表达式重构中就不会再出现了,因此可以继承IExpressionAdjustor进行事先处理,处理完成后可以将XFinder从finders中移除以提高性能。

以下是XFinder的定义(现在你终于感觉到重构带来的优惠了吧!嘻嘻):

public class XFinder : FinderBase,IExpressionAdjustor
    {
        public override int Priority { get { return (int)FinderPriority.XFinder; } }

        protected override string Rule
        {
            get { return @"x"; }
        }

        protected override INode GenerateNode(string sourceExpression, string data, int index)
        {
            if (sourceExpression.Length > Match.Index + Match.Value.Length)
            {
                var tailChar = sourceExpression[Match.Index + Match.Value.Length];
                //如果后面一个字符是':'或者是[a-zA-Z],则它是一个函数中的字符,不当做变量处理,否则当做变量
                if (tailChar == '(' || (tailChar >= 'a' && tailChar <= 'z') || (tailChar >= 'A' && tailChar <= 'Z'))
                    return null;
            }
            return new XNode(index, data, sourceExpression);
        }

        //重构原始表达式,并生成每个函数的Finder实例
        public void AdjustExpression(ref string expression, ref List<IFinder> finders)
        {
            while (true)
            {
                INode node = Find(expression);
                if (node == null) break;
                AddNode(Calculator.FoundNodes, node);
                expression = expression.ReplaceOnce(node.Value, node.Id, node.Index);
            }
            finders = finders.Except(new List<IFinder> { this }).ToList(); //当前类的职责已经结束,将其移除
        }
    }

由于含变量的表达式无法直接求值,CalculatorCalculateExpression方法也就不够用了,我们需要提供另外一个方法:GetValue(double x)以实现对变量的先赋值再取值。

以下是更新后的Calculator类,可以看出为了使用方便我们还提供了一个GetValues()的方法:

    public class Calculator
    {
        private List<INode> _foundNodes;
        public List<INode> FoundNodes { get { return _foundNodes; } }
        public INode RootNode { get { return FoundNodes.Last(); } }

        public double CalculateExpression(string expression)
        {
            _foundNodes = new List<INode>();
            FinderBase.FindAllNodes(this, ref expression);

            if (FoundNodes != null && FoundNodes.Count >= 1)
                return RootNode.GetValue();

            return double.NaN;
        }

        public double GetValue(double x)
        {
            XNode.XValue = x;
            return RootNode.GetValue();
        }

        //例如可以返回Points:(x1,y1),(x2,y2)...
        public List<Tuple<double, double>> GetValues(double xFrom, double xTo, int steps)
        {
            double oneStep = (xTo - xFrom)/steps;
            var rlt = new List<Tuple<double, double>>();
            for (int i = 0; i < steps; i++)
            {
                XNode.XValue = xFrom + oneStep*i;
                RootNode.GetValue();
                rlt.Add(new Tuple<double, double>(XNode.XValue, RootNode.GetValue()));
            }
            return rlt;
        }

        internal INode GetNode(string id)
        {
            return FoundNodes.FirstOrDefault(n => n.Id == id);
        }
    }

你可以注意到,第一次调用GetValue()方法之前必须先调用CalculateExpression()进行节点解析,此后就不需要进行解析了,因为直接使用解析出来的Nodes就可以了。

怎么展示X的美妙之处呢?对,画图演示,下面来说说平面直角坐标系:

数学上的平面直角坐标系与电脑的屏幕坐标系的差别不用说了吧,归纳一下是以下三点:

  1. Y轴方向相反。
  2. 原点位置(屏幕或者说UI控件如(Canvas)的坐标系原点在左上角顶点,数学坐标系通常在中心)
  3. 单位(屏幕坐标系通常以像素为单位,数学坐标系通常以单元(例如以1厘米为一个单元))

好了,有了这些差别,势必涉及到数学转换,例如屏幕上的位置(PhysicalPoint)转换为数学逻辑上的位置(LogicalPoint)。.net已经有了Point类型表示一个位置,我们可以直接用它,不过在这里为了避免混淆,我们还是先分别定义PhysicalPointLogicalPoint两个类吧,以免将来名不正言起来不顺啦:

 

    public class LogicalPoint:PointBase
    {
        public LogicalPoint (double x,double y) : base(x,y){}
        public override PhysicalPoint ToPhysical(CoordinateSystem cs)
        {
           return  cs.ToPhysical(this);
        }
    }

    public class PhysicalPoint : PointBase
    {
        public PhysicalPoint(double x, double y) : base(x,y){}
        public override LogicalPoint ToLogical(CoordinateSystem cs)
        {
            return cs.ToLogical(this);
        }
    }

转换必然会用到CoordinateSystem,就是我们定义的数学坐标系,因为我们可能会用到多个坐标系,而每个的单位长度可能不一样,以下是的CoordinateSystem定义,我们直接继承Canvas,因为坐标系通常要画坐标轴和刻度嘛,当然也可以采用聚合的方法,将Canvas作为CoordinateSystem的一个属性来实现。

public class CoordinateSystem:Canvas
    {
        public CoordinateSystem(double width,double height)
        {
            this.Width = width;
            this.Height = height;
            Origin = new PhysicalPoint(width/2, height/2);
        }

        private PhysicalPoint Origin;
        private const double UnitLength = 50;//每单位长度对应的屏幕像素

        #region Coordinate transforms

        public LogicalPoint ToLogical(PhysicalPoint p)
        {
            return new LogicalPoint((p.X - Origin.X) / UnitLength, -(p.Y - Origin.Y) / UnitLength);
        }

        public PhysicalPoint ToPhysical(LogicalPoint p)
        {
            return new PhysicalPoint(Origin.X + p.X * UnitLength, Origin.Y - p.Y * UnitLength);
        }

        #endregion
    }

好了,至于在Canvas上画线啦、点啊什么的,不需要我多嘴了,Silverlight的特长,呵呵。不清楚的话可以直接看代码!

下面是运行截图,是不是比以前酷多了?

好啦,X之谜就是如此简单,但是为什么只有点,任然没有见到传说中的曲线呢?你可能已经猜到了:实现曲线还是有难度滴!如果我们直接将点与点用线段连起来,那对于像Sin(x)这样的表达式没问题,如果对于Sin(1/x)呢?或者更简单点直接对于y=1/x呢?悲剧了悲剧了!这涉及到求有效区间的问题,寡人暂时还没有想出来完全可行的方案,希望网友知道的话不吝赐教啊,在此谢过!

【源代码和演示地址】

 下一部分我们将对之前的所作所为做个总结,具体是加入动画演示来更形象的表述表达式解析和求值的过程,从而达到教学的目的!

posted @ 2012-11-24 11:37  地月银光  阅读(1512)  评论(2)    收藏  举报