基於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軸是向下的。(原點在左上角)在轉移到屏幕上顯示的時候,是要有一點小變化的。所以上邊我們求出來的結果,是在屏幕座標系下,相對於常用座標系的偏移量(確實有點繞)

爲了讓大家方便理解代碼,我把計算公式抽取出來

知道了方向標的方向,下一步就是方向標的位置,這裏我們的位置是以方向標的方向角的點(方向標是一個三角形,指向方向的角我在這裏叫方向角)向對邊的垂線的點,取這個點爲參考點(因爲這個點落在路徑上,其他三點都在路徑外。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。。。這一章講的有點多,大家慢慢消化吧。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章