基于ios的室内地图基本绘制 —— (2)路径的绘制

上一篇结束后,我们在界面上已经可以画出商家大头针了,但是,地图嘛,还是少不了规划的路径,今天讲一下路径的绘制吧。

单纯的路径绘制,还是很方便的,只要有这条路径上的各个点(起始点,拐点,终点)给予出事的路径宽度,连起来就好了,但是这只是一个“光秃秃”的路径。

先让我们实现这个“光秃秃”的路径吧!

先上代码吧,看着代码说比较直接,这一部分也没有什么难度

for (NSDictionary * dic in self.points) {
        
        NSString * pointx = dic[@"x"];
        NSString * pointy = dic[@"y"];
        
        ZHPoint * point = [ZHPoint initWithX:pointx.doubleValue / indoorImage.size.width * oldFrame.width y:pointy.doubleValue / indoorImage.size.height * oldFrame.height width:0];
        
        if(isFirst){
            
            // 起点
            [linePath moveToPoint:CGPointMake(point.x, point.y)];
            isFirst = false;
            
        }else {
            
            [linePath addLineToPoint:CGPointMake((float)point.x, (float)point.y)];
            
        }
        
        index ++;
        
    }

首先self.points是存放路径上的各个点,接下来的三行代码是对点数组进行转化,然后我们开始绘制路径。

这里linePath的类型是UIBezierPath的,也就是说我们这里是用UIBezierPath来绘制路径,下面的代码有一个判断语句,是用来判断当前点是不是数组中第一个点,也就是起始点,然后把一个个点加入其中,然后直接绘制,万事ok了。

然后在缩放的监听事件里,对路径的宽度做出改变

self.defaultLineWidth = self.defaultLineWidth / pinchGestureRecognizer.scale;

从上一篇中我们已经分析了单个控件的大小变化,这里不再赘述。

emmmm。。。路径是画上去了,但是,没有方向箭头,对用户来说不太好吧,虽然可以标明起始点和终点,来表示方向,但是,一是不够直观,再者如果把地图放大看详细的路线的话,屏幕上没有起始点终点的话,用户可能会暂时忘记方向,对用户来说操作不便,再退一步说,百度高德啥的反正都有吧,好歹得做做吧。

emmmm。。。让我们来分析一下,怎么加方向标吧。

加方向标其实还是有很多问题需要解决的:

1.方向标的方向的确立

2.方向标加载的位置

3.方向标在这条路径上的个数

4.方向标个数的变化(缩放地图,多线程加载)

我们一个一个的说:

1. 方向标方向的确立

这个上来就是一个数学运算问题吧,我们首先在数组里取到相邻的两个点,我们肯定是要知道这两个点的顺序的,我们是根据数组的顺序来判断方向标的方向的,比如数组X[n],n=1,2,3,4…n  我们取出x1和x2。从人的角度来思考,从x1到x2画一条射线,这个射线的方向,便是我们要的方向标的方向。这么看确实是很直观,但是从机器的角度来算,就很麻烦了,因为我们要计算角度

 

这里的计算可能比较绕,但是重要的是算法思想,先简单的说一下算法的思想:

首先根据两个点的相关信息的求出偏移角度(可能存在座标轴变化的问题,可以对于座标轴的变化而变化,也可以不变。变得话,就是座标轴变为屏幕座标轴,但是角度的偏移量是根据常用座标轴来计算的。不变的话,就是还是常用座标轴求出来的偏移量,在新的座标轴仍然是通用的,只不过在后面求解方向标的三个点的时候会有不同的处理,下面展示的是我采取变化的代码,还是有些冗余和绕的)

 

上代码

// 计算两个点之间偏离的角度
+ (double)calcDirectionIconOffset:(ZHPoint *)point1 and:(ZHPoint *)point2{
    
    if(point2.x - point1.x == 0){
        
        if(point2.y < point1.y){
            
            return M_PI_2;
            
        }else{
            
            return -M_PI_2;
            
        }
        
    }
    
    double calcK = fabs((point2.y - point1.y) / (point2.x - point1.x));
    
    double offset = atan(calcK);
    
    if(point2.x < point1.x){
        
        if(point2.y > point1.y){
            
            offset = M_PI - offset ;
            
        }else{
            
            offset = -M_PI + offset;
            
        }
        
    }else{
        
        if(point2.y < point1.y){
            
            return  offset;
            
        }
        
    }
    
    return -offset;
    
}

上面的代码仅仅是根据两个座标点(区分先后顺序,前面的参数先,后面的参数后)的x,y值来判断线的偏离角度。但是这里我们要强调一个问题,这个问题比较绕,这里我们计算的偏离角度,不是根据我们小学中学常见的x,y座标系,就是在一个平面内,x轴是向左的,y轴是向上的,然而我们在ios系统的手机屏幕上,x轴是没有变化的,但是y轴是向下的。(原点在左上角)在转移到屏幕上显示的时候,是要有一点小变化的。所以上边我们求出来的结果,是在屏幕座标系下,相对于常用座标系的偏移量(确实有点绕)

为了让大家方便理解代码,我把计算公式抽取出来

《基于ios的室内地图基本绘制 —— (2)路径的绘制》

知道了方向标的方向,下一步就是方向标的位置,这里我们的位置是以方向标的方向角的点(方向标是一个三角形,指向方向的角我在这里叫方向角)向对边的垂线的点,取这个点为参考点(因为这个点落在路径上,其他三点都在路径外。ps:这里的路径是指的路径座标点连成的细线,不考虑路线的宽度的) 说的有点拗口,画个图吧

《基于ios的室内地图基本绘制 —— (2)路径的绘制》

这个三角形是方向标,红色的地方是方向角(指向方向的角,这个方向标指向的是右边)

从这个角向对应的边做垂线,即是蓝色点,这个点就是在路径上的点。我们这个垂线也全部都在路径细线上。

 

2,3方向角的位置和个数

这两个我们一起说,因为一般情况下,在整个路径上,先确认要放多少个点,根据个数平分路径,或者类似的思路,确定相邻两个方向标之间的距离,依次放点。我们在这里采取的是第二种方法。

这种方法的具体思路就是,相邻两个方向标之间的距离,依次放点,遇到拐点的地方,记录剩余距离,在新的拐点处继续计算放置位置

这里放置方向标的时候,需要先计算两个相邻路径座标之间的距离,如果距离大于方向标之间的距离的话,放置方向标,否则记录距离,从下个拐点开始,继续比较:

计算两个点之间的距离,具体代码如下:

// 计算两个点之间的距离
+ (double)calcDistanceOfTwoPoints:(ZHPoint *)point1 and:(ZHPoint *)point2{
    
    return sqrt( pow((point2.x - point1.x), 2) + pow((point2.y - point1.y), 2) );
    
}

咳咳。。这个就不需要解释了吧。

然后就是我们这个放置方向标的具体流程,具体代码如下

int index = 0;
    
    ZHPoint * tempPoint;
    CGFloat tempDistance = self.defaultDirectionDistance;
    //    for (ZHNagivationPoint * point in self.points) {
    //
    //        NSString * pointx = [NSString stringWithFormat:@"%lf", point.x];
    //        NSString * pointy = [NSString stringWithFormat:@"%lf", point.y];
    
    for (NSDictionary * dic in self.points) {
        
        NSString * pointx = dic[@"x"];
        NSString * pointy = dic[@"y"];
        
        ZHPoint * point = [ZHPoint initWithX:pointx.doubleValue / indoorImage.size.width * oldFrame.width y:pointy.doubleValue / indoorImage.size.height * oldFrame.height width:0];
        
        if(index == 0){
            
            tempPoint = point;
            index++;
            continue;
            
        }
        
        CGFloat currentTwoPointDistance = [ZHRoutePlanning calcDistanceOfTwoPoints:tempPoint and:point];
        
        ZHPoint * middlePoint = tempPoint;
        
        while (currentTwoPointDistance >= tempDistance) {
            
            currentTwoPointDistance -= tempDistance;
            
            middlePoint = [ZHRoutePlanning calcTwoPointBuildLineOnePointInDistance:middlePoint and:point distance:tempDistance];
            
            UIImage * directIcon = [self drawDirectionIcon:middlePoint andOffset:[ZHRoutePlanning calcDirectionIconOffset:tempPoint and:point] radius:self.defaultDirectionWidth];
            
            [array addObject:directIcon];
            
            tempDistance = self.defaultDirectionDistance;
            
        }
        
        tempDistance -= currentTwoPointDistance;
        
        tempPoint = point;
        index++;
        
    }

这里需要解释一下了,第一行代码是一个记录路径座标下标的变量,tempDistance=self.defaultDirectionDistance,self.defaultDirectionDistance是指系统指定的相邻连个方向标之间的距离,这个值会在缩放地图的时候发生变化。随后进入for循环,遍历路径座标。然后三行代码实例化座标点,便于计算。如果是第一个值得话,保存到相关变量中,这样后边的for循环中,就可以每次和上一个(相邻的座标)来计算路径长度。然后计算两个路径座标之间的距离。然后开始距离的比较,如果路径座标之间的距离大于方向标之间的距离,说明可以放置一个方向标,然后我们计算放置的位置(计算代码会在后边给出),然后画在界面上(计算代码会在后边给出)。更新相关的变量(下次计算方向标,起始点的位置要改变,改成这次计算出来的方向角的参考点。路径的距离也要变,即新的起始点到另一个路径座标的距离,距离即是currentTwoPointDistance -= tempDistance;)然后把画好的点加入到数组中去。而如果方向标之间的距离大于路径座标之间的距离的话,记录剩余距离(tempDistance -= currentTwoPointDistance;)更新上一个相邻的座标点,下标+1,继续for循环

下面给出计算方向标位置的算法

// 根据距离(假设长度为L)和两个点(起始点,终点)的方向,计算从起始点到终点为L的点的座标
+ (ZHPoint *)calcTwoPointBuildLineOnePointInDistance:(ZHPoint *)point1 and:(ZHPoint *)point2 distance:(CGFloat)distance{
    
    ZHPoint * point;
    
    if(point2.x - point1.x == 0){
        
        if(point2.y > point1.y){
            
            point = [ZHPoint initWithX:point1.x y:point1.y + distance width:0];
            
        }else{
            
            point = [ZHPoint initWithX:point1.x y:point1.y - distance width:0];
            
        }
        
    }
    
    double offset = [self calcDirectionIconOffset:point1 and:point2];
    
    if(point2.x > point1.x){
        
        if(point2.y < point1.y){
            
            point = [ZHPoint initWithX:point1.x + distance * cos(offset) y:point1.y - distance * sin(offset) width:0];
            
        }else{
            
            offset = -offset;
            
            point = [ZHPoint initWithX:point1.x + distance * cos(offset) y:point1.y + distance * sin(offset) width:0];
            
        }
        
    }else{
        
        if(point2.y < point1.y){
            
            offset = M_PI_2 - (offset - M_PI_2);
            
            point = [ZHPoint initWithX:point1.x - distance * cos(offset) y:point1.y - distance * sin(offset) width:0];
            
        }else{
            
            offset = M_PI + offset;
            
            point = [ZHPoint initWithX:point1.x - distance * cos(offset) y:point1.y + distance * sin(offset) width:0];
            
        }
        
    }
    
    return point;
    
}

这里没有什么难度吧,就是计算一下斜率行方向,考虑一下特殊情况(斜率为无限大,即特殊垂直情况),返回座标点的位置即可。

画方向角的代码

// 绘制单个 方向icon
- (UIImage *) drawDirectionIcon:(ZHPoint *)point andOffset:(double)offset radius:(CGFloat)radius{
    
    offset = -offset;
    
    double radiusY = radius;
    double radiusX = sqrt(pow(radiusY * 2, 2) - pow(radiusY, 2));
    
    //开始图像绘图
    UIGraphicsBeginImageContextWithOptions(oldFrame, NO, 4.0);
    
    UIColor *color = [UIColor whiteColor];
    [color set]; //设置线条颜色
    UIBezierPath *aPath = [UIBezierPath bezierPath];
    aPath.lineWidth = 1.0; //设置线宽
    aPath.lineCapStyle = kCGLineCapRound; //线条拐角
    aPath.lineJoinStyle = kCGLineCapRound; //终点处理
    [aPath moveToPoint:CGPointMake(point.x + radiusY * cos(offset + M_PI_2),point.y + radiusY * sin(offset + M_PI_2))];
    [aPath addLineToPoint:CGPointMake(point.x +  radiusX * cos(offset),point.y + radiusX * sin(offset))];
    [aPath addLineToPoint:CGPointMake(point.x +  -radiusY * cos(offset + M_PI_2),point.y + -radiusY * sin(offset + M_PI_2))];
    //[aPath closePath];
    [aPath stroke];//根据座标点连线 ([aPath fill];填充)
    
    //从Context中获取图像,并显示在界面上
    UIImage *img = UIGraphicsGetImageFromCurrentImageContext();
    UIGraphicsEndImageContext();
    //UIImageView *imgView = [[UIImageView alloc] initWithImage:img];
    
    return img;
    
}

这里的关键代码就是一开始我们对偏移角度取反,方便后边的计算,计算方向标两条等边的边长和垂线的长度(还记得之前那个三角形嘛)。我们设计的方向标是一个等边三角形,边长为2a,即是radius,那垂线的长度即是radiusX那里的计算过程,然后三个点的计算过程在12-14行代码上。我们把三个点连起来,便形成了一个三角形。

相应的公式如下

《基于ios的室内地图基本绘制 —— (2)路径的绘制》

4. 方向标的加载

emmm。。。忙活这么半天了,方向标也画出来了,感觉基本上要大功告成了,结果在加载过程中,出了问题了。这个问题就是,方向标太多了,一开始的直接加载其实还好,一旦缩放地图,改变方向标相邻距离的大小(self.defaultDirectionDistance = self.defaultDirectionDistance / pinchGestureRecognizer.scale;)方向标的个数就会发生变化,就是在这个时候。如果你是直接加载的话,那我只能很抱歉的告诉你,你的手机要不卡的要死,要不就是直接闪退(我实在iphone7上测试的,还顽强的卡了一会,没有直接卡死闪退,舍友的iphone6表示根本不想反抗,直接闪退的说)。为什么呢,你可以想一下,其实加载一个照片的过程韩式很消耗cpu使用率和内存的,我们平时浏览新闻,微博的时候,可以发下这一类的app还是有一个共同的特点的,要不就是下拉加载新数据,要么就是下拉是空白的,自动加载,不可能你直接点进一个页面(app),数据全部加载完,而且下拉永远没有底的(你手机再新,内存再大也受不了的),当然也有这种吧所有数据放在一起的网页(emmm,加载速度你们是可想而知的)。这里,就要用到本科经常学到,但是真的到了实战阶段,你却想不起来的一个小概念了,没错,就是多线程。利用多线程,外加加锁操作(用户的手在缩放过程中,如果不撒手,就不去加载新的缩放比地图下的方向标数据数据),此外,我们还做一点小优化,把所有的方向标先放在一个image上,然后再加载,这样极大的释放了主线程的压力,节省了部分内存。

上多线程加锁代码

self.defaultDirectionWidth = self.defaultDirectionWidth / pinchGestureRecognizer.scale;
            self.defaultDirectionDistance = self.defaultDirectionDistance / pinchGestureRecognizer.scale;
            
            if(!self.flagOfZoom){
                
                self.flagOfZoom = true;
                
                dispatch_async(dispatch_get_global_queue(0, 0), ^{
                    
                    NSMutableArray * array = [self drawDirectionIcons];
                    UIImage * directionIcon = [self merge:array];
                    
                    dispatch_async(dispatch_get_main_queue(), ^{
                        
                        [self.directIconView removeFromSuperview];
                        
                        self.directIconView = [[UIImageView alloc] initWithImage:directionIcon];
                        
                        [_indoorMapView addSubview:self.directIconView];
                        
                        self.flagOfZoom = false;
                        
                    });
                    
                });
            }

这里没有什么好解释的了,主要是前面说的算法思想,这里稍微说一下,flagOfZoom是表示当前是否处于缩放状态,是用来做加锁处理的。然后加载图片的过程放在多线程当中,只有完成后再把flagOfZoom置为可执行状态。

emmm。。。这一章讲的有点多,大家慢慢消化吧。

点赞