哲学原理佛洛依德路径平滑算法(floyd)

by admin on 2018年9月25日

  常见的a*算法的结果是千篇一律差用来表示所经过的路径点坐标。但是如此的门道通常是来“锯齿”的,并无抱现实中之智能表现。

Animation.gif

故,需要越来越的开展平整处理,比如 佛洛依德算法~

github__ZXKline

  算法原理非常简短,分为两步:

1.简介篇

  • 蜡烛图和山形图绘制切换
  • 5种指标绘制切换
  • 长论蜡烛和指标线详情展示
  • 触底加载重多
  • 实时蜡烛绘制实现
  • 二级横屏和炬三级横屏

fullScreen1.png

fullScreen2.png

  • 适配两栽布局

UI1.png

UI2.png

  1.去丢相邻之共线的触及

2.原理篇

  2.错过丢多余的转弯的点

2.1 tableView作为画布依耐

  第一步实现起来很简短,只待遍历一下,计算两独向量的大势是否一致。

缘何选了tableView

  • 品是否能够对绘制有candle的Cell进行复用;
  • 变个思维去轮子;

  第二步之落实小麻烦一点,遍历所有的点,去丢两只可以直接通过之触发之间的点。

亟需缓解之题材:变纵向滚动为纵向滚动

旋转.png

  • 如图所示:在打转时,是绕tableView中心进行盘的,为了要旋转后底tableView的frame能够跟superView的高低同等,那么将使旋转前的tableView偏移一定去;

    .
    .
    self.tableView.transform = CGAffineTransformMakeRotation(-M_PI/2);
    .
    .
    [self.view addSubview:self.tableView];
    .
    .
    [self.tableView mas_updateConstraints:^(MASConstraintMaker *make) {
        make.left.mas_equalTo((width-height)/2);
        make.top.mas_equalTo(-(width-height)/2);
        make.width.mas_equalTo(height);
        make.height.mas_equalTo(width);
    }];  
  • 利弊:虽然进行到后,蜡烛都是因此CAShapeLayer+UIBeizerPath绘制的,cell的复用并不曾起至几近百般之打算,并且旋转之后涉及到了tableView的x,y坐标在使用中的变换(这点大家只顾下),但是会感到庆幸之是:使用了cell之后,在测算蜡烛横坐标的下即便是cell.indexPath.row*rowHeight;再者就是以缩放的时节,可以直接修改cell的万丈就好高达缩放的目的;

  有点绕。。。。

2.2 缩放

  其实是老经典的画直线算法,找点儿个点作为端点画一长长的线,这条先经的网格如果都是不过同行的,那么我们就算觉着于途径中马上片独点中的那些点是剩下的。

缩放有度

- (void)pinchAction:(UIPinchGestureRecognizer *)sender
{ 
    static CGFloat oldScale = 1.0f;
    CGFloat difValue = sender.scale - oldScale;
    NSLog(@"difValue=====%f",difValue);
    NSLog(@"oldScale=====%f",oldScale);
    if (ABS(difValue)>StockChartScaleBound) {

    CGFloat oldKlineWidth = self.candleWidth;
    // NSLog(@"原来的index%ld",oldNeedDrawStartIndex);
    self.candleWidth = oldKlineWidth * ((difValue > 0) ? (1+StockChartScaleFactor):(1-StockChartScaleFactor));
    oldScale = sender.scale;
    if (self.candleWidth < scale_MinValue) {

        self.candleWidth = scale_MinValue;
    }else if (self.candleWidth > scale_MaxValue)
    {
        self.candleWidth = scale_MaxValue;
    }
  }
}
  • 在每次缩放的时候,进行判定:
    1)只有点的缩放大于某个预订值的当儿才进行缩放
    2)控制每次缩放的比值;
    3)控制缩放的总体范围;

其实第二步就是足以形成优化,但是计算量比较充分。所以先经第一步来减少部分计算量~

恒定缩放

//这句话达到让tableview在缩放的时候能够保持缩放中心点不变;
//实现原理:在放大缩小的时候,计算出变化后和变化前中心点的距离,然后为了保持中心点的偏移值始终保持不变,就直接在原来的偏移上加减变换的距离
//ceil(centerPoint.y/oldKlineWidth)中心点前面的cell个数
//self.rowHeight-oldKlineWidth每个cell的高度的变化
CGFloat pinchOffsetY  = ceil(centerPoint.y/oldKlineWidth)*(self.candleWidth-oldKlineWidth)+oldNeedDrawStartPointY;
if (pinchOffsetY<0) {

    pinchOffsetY = 0;
}
if (pinchOffsetY+self.subViewWidth>self.kLineModelArr.count*self.candleWidth) {

    pinchOffsetY = self.kLineModelArr.count*self.candleWidth - self.subViewWidth;
}

[self.tableView setContentOffset:CGPointMake(0, pinchOffsetY)];

下面是代码:

2.3 实现原理

#region floyd
    //----------------------------------------弗洛伊德路径平滑--------------------------------------//

    public List<Vector3> Floyd( List<Vector3> path)
    {
        if (path == null) 
        {
            return path;
        }


        int len = path.Count;
        //去掉同一条线上的点。
        if (len > 2) 
        {
            Vector3 vector = path[len -1] - path[len - 2];
            Vector3 tempvector;
            for (int i = len - 3; i>= 0; i--) 
            {
                tempvector = path[i+1] - path[i];
                if (Vector3.Cross(vector, tempvector).y == 0f)
                {
                    path.RemoveAt(i+1);
                }
                else
                {
                    vector = tempvector;
                }
            }
        }
        //去掉无用拐点
        len = path.Count;
        for (int i = len-1; i >= 0; i--) 
        {
            for (int j = 0; j<= i-1; j++) 
            {
                if (CheckCrossNoteWalkable(path[i],path[j])) 
                {
                    for (int k = i-1; k>=j; k--)
                    {
                        path.RemoveAt(k);
                    }
                    i=j;
                    //len = path.Count;
                    break;
                }
            }
        }
        return path;
    }

    float currentY; // 用于检测攀爬与下落高度
    //判断路径上是否有障碍物
    public bool CheckCrossNoteWalkable(Vector3 p1, Vector3 p2)
    {
        currentY = p1.y; //记录初始高度,用于检测是否可通过
        bool changexz = Mathf.Abs(p2.z - p1.z) > Mathf.Abs(p2.x - p1.x);
        if (changexz) 
        {
            float temp = p1.x;
            p1.x = p1.z;
            p1.z = temp;
            temp = p2.x;
            p2.x = p2.z;
            p2.z = temp;
        }
        if (!Checkwalkable(changexz, p1.x, p1.z)) 
        {
            return false;
        }
        float stepX = p2.x > p1.x ? Tilesize : (p2.x < p1.x ? -Tilesize : 0);
        float stepY = p2.y > p1.y ? Tilesize : (p2.y < p1.y ? -Tilesize : 0);
        float deltay = Tilesize * ( (p2.z - p1.z) / Mathf.Abs(p2.x - p1.x) );
        float nowX = p1.x + stepX/2;
        float nowY = p1.z - stepY/2;
        float CheckY = nowY;

        while (nowX != p2.x)
        {
            if(!Checkwalkable(changexz, nowX, CheckY))
            {
                return false;
            }
            nowY += deltay;
            if(nowY >= CheckY + stepY)
            {
                CheckY += stepY;
                if (!Checkwalkable(changexz, nowX, CheckY)) 
                {
                    return false;
                }
            }
            nowX += stepX;
        }
        return true;
    }
    private bool Checkwalkable(bool changeXZ, float x, float z)
    {      
        int mapx = (MapStartPosition.x < 0F) ? Mathf.FloorToInt(((x + Mathf.Abs(MapStartPosition.x)) / Tilesize)) : Mathf.FloorToInt((x - MapStartPosition.x) / Tilesize);
        int mapz = (MapStartPosition.y < 0F) ? Mathf.FloorToInt(((z + Mathf.Abs(MapStartPosition.y)) / Tilesize)) : Mathf.FloorToInt((z - MapStartPosition.y) / Tilesize);
        if (mapx < 0 || mapz < 0 || mapx >= Map.GetLength(0) || mapz >= Map.GetLength(1))
        {
            return false;
        }

        Node note;
        if (changeXZ)
        {
            note = Map[mapz, mapx];
        }
        else
        {
            note = Map[mapx, mapz];
        }
        bool ret = note. walkable && ( (note.yCoord - currentY <= ClimbLimit && note.yCoord >= currentY) || (currentY - note.yCoord <= MaxFalldownHeight && currentY >= note.yCoord) );
        if (ret) 
         {
             currentY = note.yCoord;
         }
        return ret;
    }
#endregion end floyd

本布局

 

有数单至关重要参数:

  • 屏幕被显示的首先个蜡烛图的X坐标:

    NSUInteger leftArrCount = ABS(scrollViewOffsetX/self.candleWidth);
    _needDrawStartIndex = leftArrCount;      
    
  • 屏幕被能够显得的蜡个数:

     - (NSInteger)needDrawKlineCount
    {
        CGFloat width = self.subViewWidth;
        _needDrawKlineCount = ceil(width/self.candleWidth);
        return _needDrawKlineCount;
    }    
    

    据悉当下片单参数,起点与长,就得由数额源数组中准确之取出时屏幕显示的蜡烛图的多少;然后滑动过程遭到实时计算并展开坐标转换

 

坐标相关换算

  • 极值:从眼前屏幕显示的数目源数组获得之无比可怜价值和极端小值

  • 单位价格所代表的诸如素值

      self.heightPerPoint = self.candleChartHeight/(self.maxAssert-self.minAssert);  
    
  • 开收高低值从价转移成像素值

蜡烛绘制

CAShapeLayer+UIBeizerPath

2.4 Socket数据结算

详见ZXSocketDataReformer
对服务器返回的数码格式:@”时间戳,实时价格”;我们要以就一个个之多寡好构建蜡烛模型;

  • 第一模构建:假如同样分钟返回80个数据,
    那么我们要看清这无异于分钟开始的时,并且取出即同样分钟的第一单数据First,构建一个簇新的模型A;模型A的开.收.高.低价都是第一数据的实时价格;
  • 型替换:第一个模型构建之后,新的数量Second到来,那么我们比较得出高值和低值替换模型A的高低值,并且这模型A的收盘价为数量Second的实时价格;
  • 型结算(重点):
    结算:就是针对只M1\M5\M15..中回到的有数据好结算出一个蜡烛模型,也尽管是四只价:开\收\高\低;
    结算的波点判断方式:
    1)以socket返回数据的日戳结算:这样结算在数上无见面出什么误差,但是日子达会时有发生误差;
    eg:针对M1而言,假如在6’58”的下回来此分蜡烛的末梢一个值,如果就此socket的时光作结算的话,那么我们不能不等到下一个socket返回值的年月戳到才会结算,假如socket在7’00”-7’01”之间回到了数额吧,很好,我们好直接结算及一个蜡烛,并且立即的创立一个初的蜡模型;但是数量并无是历次都见面生成如此频繁,如果生一个数额的至是7’16”;那么中就18”,k线图会静止18”,那么一定给6’的不可开交蜡烛会延迟16”进行推动,便导致了岁月及的误差;并且当数涨停或者停牌的早晚,socket数据没有更改,便不会见返回数据,那么这时刻k线图也是无会见发其他动作;
    2)以要服务器时间戳结算:会促成数据及之误差;eg:在7’00”需要结算,但是这时间socket在7’00”的时回来了差不多个数据,但是结算的时段只是会获取到里头一个数作6’的收盘价,其他数以残留到下单蜡烛;
    解决:
    1)以socket和服务器的光阴戳相结合的方开展结算:我当ZXSocketDataReformer受呢是这么做的,第一糟糕呼吁服务器时间,然后本地安装定时器进行服务器时间共同;
    由socket时间穿进行模型构造,到了整点,优先socket进行模型推进,如果整点的下没有socket返回,就由于服务器时间展开推动;
    2)定时器由服务器创建,最好就算是于整点延迟1秒的下,如果以00”-01”的时候已经发生socket数据传送至运动端的话,那么就算无待推送假数据,如果没有socket数据发生,就推送一个借出数据及移动端,告诉移动端,数据要开展结算,移动端只需要为此socket进行结算;
    (好吧,自己还绕晕了,如果要求未是那强其实仅仅以socket进行多少结算也足够用了);

2.5 实时绘制

设想如下情况:

实时绘制.png

代码大概是如此的 :

- (void)handleNewestCellWhenScrollToBottomWithNewKlineModel:(KlineModel *)klineModel

{

     //==0的时候需要插入一个新的cell;否则只需要刷新最后一个cell
    if (self.isNew) {

        KlineModel *newsDataModel =  [self calulatePositionWithKlineModel:klineModel];
        [self.kLineModelArr addObject:newsDataModel];

        double oldMax = self.maxAssert;
        double oldMin = self.minAssert;


        [self calculateNeedDrawKlineArr];
        [self calculateMaxAndMinValueWithNeedDrawArr:self.needDrawKlineArr];

        //不等的话就重绘
        if (oldMax<self.maxAssert||oldMin>self.minAssert) {


            dispatch_async(dispatch_get_main_queue(), ^{

                [self.tableView setContentOffset:CGPointMake(0, (self.kLineModelArr.count-self.needDrawKlineCount)*self.candleWidth+(self.needDrawKlineCount*self.candleWidth-self.subViewWidth))];
            });

            [self drawTopKline];

        }else{
            //否则就插入
            NSIndexPath *indexPath = [NSIndexPath indexPathForRow:self.kLineModelArr.count-1 inSection:0];
            dispatch_async(dispatch_get_main_queue(), ^{

                //先增加  再偏移
                [self.tableView beginUpdates];
                [self.tableView insertRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationNone];
                [self.tableView endUpdates];
                [self.tableView setContentOffset:CGPointMake(0, (self.kLineModelArr.count-self.needDrawKlineCount)*self.candleWidth+(self.needDrawKlineCount*self.candleWidth-self.subViewWidth))];
            });

            [self delegateToReturnKlieArr];
        }

    }else{


        KlineModel *newsDataModel =  [self calulatePositionWithKlineModel:klineModel];
        NSIndexPath *indexPath = [NSIndexPath indexPathForRow:self.kLineModelArr.count-1 inSection:0];

        [self.kLineModelArr replaceObjectAtIndex:self.kLineModelArr.count-1 withObject:newsDataModel];


        CGFloat oldMax = self.maxAssert;
        CGFloat oldMin = self.minAssert;


        [self calculateNeedDrawKlineArr];
        [self calculateMaxAndMinValueWithNeedDrawArr:self.needDrawKlineArr];
        //如果计算出来的最新的极值不在上一次计算的极值直接的话就重绘,否则就刷新最后一个即可
        if (oldMax<self.maxAssert||oldMin>self.minAssert) {

            [self drawTopKline];

        }else{

            dispatch_async(dispatch_get_main_queue(), ^{

                [self.tableView beginUpdates];
                [self.tableView reloadRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationNone];
                [self.tableView endUpdates];
                [self delegateToReturnKlieArr];
            });

        }

    }

}

骨子里运用过程中以insert或者reloadrows的早晚,偶尔会出现崩溃,暂时还从未解决,索性改以直接重绘全屏了(我心目也是拒绝的),若是你们吗不愿被其直接重绘,可至–ZXMainView.m—
(void)handleNewestCellWhenScrollToBottomWithNewKlineModel:(KlineModel
*)klineModel;打开注释的主意,终结了它们;

3.使用篇

3.1 基本使用

  • 骨干的k线图的交接可以当demo中SecondStepViewController遭受视,运行需要于appDelegate中切换rootViewController;
  • JoinUpSocketViewController凡通socket实时绘制的demo,为了脱敏,控制器中的socket数据是擅自产生的;
  • 现实的连代码或者接口都可以在demo中看到,这里不开了多描述;

3.2 使用注意

3.2.1 历史数据转模型

(详见ReformerZXCandleDataReformer)
当地历史数据格式为:

/*
 @[@"时间戳,收盘价,开盘价,最高价,最低价,成交量",
 @"时间戳,收盘价,开盘价,最高价,最低价,成交量",
 @"时间戳,收盘价,开盘价,最高价,最低价,成交量",
 @"...",
 @"..."];
 */  

相应的范转换格式为:

- (NSArray<KlineModel *>*)transformDataWithDataArr:(NSArray *)dataArr currentRequestType:(NSString *)currentRequestType
{
    self.currentRequestType = currentRequestType;
    //修改数据格式  →  ↓↓↓↓↓↓↓终点到啦↓↓↓↓↓↓↓↓↓  ←
    NSMutableArray *tempArr = [NSMutableArray array];
    __weak typeof(self) weakSelf = self;
    [dataArr enumerateObjectsUsingBlock:^(NSString *dataStr, NSUInteger idx, BOOL * _Nonnull stop) {

        NSArray *strArr = [dataStr componentsSeparatedByString:@","];
        KlineModel *model = [KlineModel new];
        model.timestamp  = [strArr[0] integerValue];
        model.timeStr = [weakSelf setTime:strArr[0]];
        model.closePrice = [strArr[1] doubleValue];
        model.openPrice = [strArr[2] doubleValue];
        model.highestPrice = [strArr[3] doubleValue];
        model.lowestPrice = [strArr[4] doubleValue];
        if (strArr.count>=6) {

            model.volumn = @([strArr[5] doubleValue]);
        }else{
            model.volumn = @(0);
        }

        model.x = idx;
        [tempArr addObject:model];
        model = nil;
    }];
    return tempArr;
}

历史数据模型转换需要使用者根据请求历史数据的实际格式进行转换;

3.2.2 Socket数据转模型

(详见ZXSocketDataReformer)
以socket结算的下,若得服务器时间成socket返回的时间共同完成一个蜡烛的时节,这里用转移吧博得服务器时间;

- (void)requestServiceTime:(void(^)(NSInteger timesamp))success
{

        //这里Demo使用的本地时间代替;正确的应该取下面的服务器时间
        NSDate *date = [NSDate dateWithTimeIntervalSinceNow:0];
        NSTimeInterval timestamp = [date timeIntervalSince1970];
        success(timestamp);

        //获取服务器时间
    //    NSString *urlStr = @"服务器时间校对地址";
    //
    //    self.manager.responseSerializer = [AFHTTPResponseSerializer serializer];
    //    self.manager.responseSerializer.acceptableContentTypes = [self.manager.responseSerializer.acceptableContentTypes setByAddingObject:@"text/html"];
    //    [self.manager GET:urlStr parameters:nil progress:nil success:^(NSURLSessionDataTask * _Nonnull task, id  _Nullable responseObject) {
    //
    //        NSString *time = [[NSString alloc] initWithData:responseObject encoding:NSUTF8StringEncoding];
    //        success([time integerValue]);
    //        //        NSLog(@"ServiceTime=%@",time);
    //
    //    } failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
    //
    //    }];

}

3.2.3 布局修改

(详见ZXHeader.h)

完整布局修改的几乎独巨大

/**
 * 价格坐标系在右边?YES->右边;NO->左边
 */
#define PriceCoordinateIsInRight YES     

/**
 * 蜡烛的信息配置的位置:YES->单独的view显示在view顶部;NO->弹框覆盖在蜡烛上
 */
#define IsDisplayCandelInfoInTop NO

约束

布局.png

  • 内CandleChartHeight、QuotaChartHeight、MiddleBlankSpace都是可变的,所以分了横竖屏分别定义;其他尺寸都是一贯的。
  • 是因为在其中就本着各个控件的UI进行了组装,所以就是留下了连带的尺码约束还是颜色宏,可以以ZXHeader文件中展开修改,如一旦有非能够改的处在,就惟有去ZXAssemblyView.m文件中展开修改了;

从某种角度上来说,很多约束可以不改,但是宏中的TotalHeight必须根据项目需求进行修改

3.2.4 横竖屏适配

稍加技巧:因为自己此横屏之后是全屏并且隐藏了状态栏和导航栏的,为了旋转之后与竖屏的其余控件互不干扰,可以用assenblyView实例补充加在self.view的最为顶层,然后转过去以后就是直接以其它控件覆盖于底部

4 其他问题

  1. 关于历史k线和socket衔接处暂不开展处理, 衔接还存误差;
  2. 未知bug?待挖掘;
  3. k线图UI很粗略,除了k线没有另外定制,但是接口都是两全之,主要是道关乎UI部分我举行得更为少,通用性就愈强;
  4. 感谢Star;
  5. 发外其它题材欢迎Issues要么简书留言;
  6. 超链:

  7. github地址ZXKline

  8. Json转模型Mac版ESJsonFormatForMac

发表评论

电子邮件地址不会被公开。 必填项已用*标注

网站地图xml地图