上一篇结束后,我们在界面上已经可以画出商家大头针了,但是,地图嘛,还是少不了规划的路径,今天讲一下路径的绘制吧。
单纯的路径绘制,还是很方便的,只要有这条路径上的各个点(起始点,拐点,终点)给予出事的路径宽度,连起来就好了,但是这只是一个“光秃秃”的路径。
先让我们实现这个“光秃秃”的路径吧!
先上代码吧,看着代码说比较直接,这一部分也没有什么难度
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轴是向下的。(原点在左上角)在转移到屏幕上显示的时候,是要有一点小变化的。所以上边我们求出来的结果,是在屏幕坐标系下,相对于常用坐标系的偏移量(确实有点绕)
为了让大家方便理解代码,我把计算公式抽取出来
知道了方向标的方向,下一步就是方向标的位置,这里我们的位置是以方向标的方向角的点(方向标是一个三角形,指向方向的角我在这里叫方向角)向对边的垂线的点,取这个点为参考点(因为这个点落在路径上,其他三点都在路径外。ps:这里的路径是指的路径坐标点连成的细线,不考虑路线的宽度的) 说的有点拗口,画个图吧
这个三角形是方向标,红色的地方是方向角(指向方向的角,这个方向标指向的是右边)
从这个角向对应的边做垂线,即是蓝色点,这个点就是在路径上的点。我们这个垂线也全部都在路径细线上。
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行代码上。我们把三个点连起来,便形成了一个三角形。
相应的公式如下
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。。。这一章讲的有点多,大家慢慢消化吧。