如何實作原生iOS熱度圖 CG直接做圖+MapKit 從0到Double系列

jamesdouble發表於2018-04-27

如何實作原生iOS熱度圖 CG直接做圖+MapKit   從0到Double系列

本教程使用Swift 3.1, Xcode 8.0

程式碼:https://github.com/jamesdouble/JDSwiftHeatMap


現在Iphone使用者常使用的地圖外掛,不外乎就是高德與百度,國外則是Google,看來看去就是沒啥人在用本地端自帶的MKMapView,一個原因是起步晚所以欠缺很多使用者經驗跟資料,再來一個我自己認為是現成API極少,MKMapView基本上只有Annotaion,Overlay是Developer可以自訂的,而百度有軌跡,雷達...等已經是現成的API。

於是我越想越不順心,要用還是要用我們IOS原生自帶的,在網上搜了一圈只看到一個用OC寫的古老專案,用起來總不順心,現在想經由開源的方法彙整大家意見來提高整體的自由度跟使用性。

熱度圖

熱度圖是早期(1991)就已經出現的資料表達形式(矩陣表示),其成熟度以及相對應衍生影像也是相對於其他的地圖表達方式成熟。

熱度圖種類 - source:WIKI

前言

實作起來不需要用太廣的知識或是什麼深不見底的技術,基本上只要熟悉兩個區塊:

  1. MapKit : 這個當然是必須的,畢竟我們是要建立在原生的地圖上,但基本的如何新增Overlay,OverlayRender...等,這篇文章不會做太多解釋。

  2. CGContext : 也就是指***Core Graphic***, 這塊應該是不管走到哪都會碰到的冤家,不外乎就是塗鴉著色啦~

使用者Input

利用Delegate取得資料點的經緯度、影響範圍跟影響力。

HeatMap on MapKit - 記錄位置

MapKit該做的就是MapKit“能”做的,記錄相關的地理資料,包括資料的“經緯度座標“以及距離。

  1. MKOVeraly:很明顯,熱度圖這樣超級不規則的圖形,MKCircle,MKPolyline,MKPolygon...等,並不能滿足我們需要的,還是得從最根本的MKOverlay重新創造一個子類別。

    JDHeatOverlay

    • 計算Overlay的BoudingMapRect(涵蓋範圍):
    /**
     有新的點加進來 ->
     重新計算這個Overlay的涵蓋
     */
    override func caculateMaprect(newPoint:JDHeatPoint){
        var MaxX:Double = -9999999999999
        var MaxY:Double = -9999999999999
        var MinX:Double = 99999999999999
        var MinY:Double = 99999999999999
        if let BeenCaculatedMapRect = CaculatedMapRect{
            //非首次計算 -> 把上次計算的MapRect拿出來,比MaxX,Y MinX,Y
            MaxX = MKMapRectGetMaxX(BeenCaculatedMapRect)
            let heatmaprect = newPoint.MapRect
            let tMaxX = MKMapRectGetMaxX(heatmaprect)
            MaxX = (tMaxX > MaxX) ? tMaxX : MaxX
            .
            .
            //每次計算新的資料點,MapRect都會變大。}
        else{
            //首次計算 -> 取第一個點的Maprecr
            let heatmaprect = newPoint.MapRect
            .
            .        }
        let rect = MKMapRectMake(MinX, MinY, MaxX - MinX, MaxY - MinY)
        self.CaculatedMapRect = rect
    }
    複製程式碼
  2. 同理,現有的OverlayRender都無法滿足,我們要的形狀,所以也是重新定義一個類別。

    JDHeatOverlayRender

    • draw是這個類最重要的Func,再之後Core Graphic 那段一起寫。
    override func draw(_ mapRect: MKMapRect, zoomScale: MKZoomScale, in context: CGContext)
    複製程式碼

過渡(MapKit -> Core Graphic)

熟悉MapKit的朋友們一定都知道,MKMapRect與CGRect的差別,也清楚他的轉換方法,通常只會在上述的**" draw ",也就是要畫的時候進行轉換,但我這邊必須提早進行,因為我必須先知道我要畫什麼,所以我這裡自帶一個名詞*[ RowFormData ]***。

過度過程

  • 使用者資料叢集轉換前:

    單位:MKMapRect,位置:MKMapPoint,範圍:KilloMeter,原點:很大

  • 使用者資料轉換後:

    單位:CGRect,位置:CGPoint,範圍:CGFloat,原點:(0,0)

    複製程式碼

//JDOverlayRender override func caculateRowFormData(maxHeat level:Int)->(data:[RowFormHeatData],rect:CGRect)? { var rowformArr:[RowFormHeatData] = [] // for heatpoint in overlay.HeatPointsArray { //將整個叢集轉換成CGRect let mkmappoint = heatpoint.MidMapPoint let GlobalCGpoint:CGPoint = self.point(for: mkmappoint) let OverlayCGRect = rect(for: overlay.boundingMapRect) //將原點化成(0,0) let localX = GlobalCGpoint.x - (OverlayCGRect.origin.x) let localY = GlobalCGpoint.y - (OverlayCGRect.origin.y) let loaclCGPoint = CGPoint(x: localX, y: localY) //將半徑轉乘CGFloat let radiusinMKDistanse:Double = heatpoint.radiusInMKDistance let radiusmaprect = MKMapRect(origin: MKMapPoint.init(), size: MKMapSize(width: radiusinMKDistanse, height: radiusinMKDistanse)) let radiusCGDistance = rect(for: radiusmaprect).width //儲存新的資料集 let newRow:RowFormHeatData = RowFormHeatData(heatlevel: Float(heatpoint.HeatLevel) / Float(level), localCGpoint: loaclCGPoint, radius: radiusCGDistance) rowformArr.append(newRow) } let cgsize = rect(for: overlay.boundingMapRect) return (rect:cgsize,data:rowformArr) } ```

計算層:將RowFormData->CGImage

我們有了RowFormData後,就能開始計算什麼位置放什麼顏色,我們這裡自創一個簡易的類別,來幫助我們區隔該做的事:

RowDataProducer

這邊會用到的Core Graphic並不是一般常見的UIGraphicsBeginImageContext之後,GetContext在做movePoint,addArc,addPath....等,因為要再次強調我們圖層的形狀是超級不規則,甚至還要計算顏色。

超級踩坑區

超級踩坑區

超級踩坑區

我們要用的是CGContex裡的建構式

熒幕快照 2017-07-15 下午2.57.58.png

引數有data,width,height,bitsPerComponent,bytesPerRow,space,bitmapInfo 該怎麼看呢? (對於圖片概念不熟悉的朋友,我在這也扯不完,網上搜尋Bitmap或Pixels還有RGB應該就很多了。)

Color Bitmap http://jbrd.github.io/2008/02/01/bitmap-and-indexed-images.html

引數只要配對錯誤就會報錯,而且不會跟你說錯哪

  • 上圖的width,height已經有了,就是剛剛計算出來的CGRect

  • CGColorSpace & BitmapInfo:這兩個引數相輔相成,就是告訴它你的data會以什麼樣的形式呈現,以RGB或是灰階...等,上面的圖片是RGB,我們要用的也是RGB***(space = CGColorSpaceCreateDeviceRGB())***,但是多了一個值Alpha這個值大家,bitmapInfo = CGImageAlphaInfo.premultipliedLast.rawValue,這告訴它alpha直放在最後 -->

    也就是一個Pixel格式(R G B A)

  • 有了Pixel格式就知道它的大小,四個值都是0~255所以是8個Bits(BitsPerComponent),一個Pixel就是8 * 4 =32Bits (4Bytes),bytesPerRow = 4 * width。

得知Data格式是大小 (4 x width) x height的 UTF8Char(大小剛好是8bits)陣列。

回到程式碼:

override func produceRowData()
    {
        var ByteCount:Int = 0
        for h in 0..<self.FitnessIntSize.height
        {
            for w in 0..<self.FitnessIntSize.width
            {
                var destiny:Float = 0
                for heatpoint in self.rowformdatas
                {
                    let pixelCGPoint = CGPoint(x: w, y: h)
                    //計算每個資料點對這個pixel的密度影響
                }
                .
                .
                let rgb = JDRowDataProducer.theColorMixer.getDestinyColorRGB(inDestiny: destiny)
                
                let redRow:UTF8Char = rgb.redRow
                let greenRow:UTF8Char = rgb.greenRow
                let BlueRow:UTF8Char = rgb.BlueRow
                let alpha:UTF8Char = rgb.alpha
                //存入4個Byte進RowData
                self.RowData[ByteCount] = redRow
                self.RowData[ByteCount+1] = greenRow
                self.RowData[ByteCount+2] = BlueRow
                self.RowData[ByteCount+3] = alpha
                ByteCount += 4
            }
        }
    }
複製程式碼

有了Data回到Render

	override func draw(_ mapRect: MKMapRect, zoomScale: MKZoomScale, in context: CGContext) {
        
        func getHeatMapContextImage()->CGImage?
        {
            //More Detail
            func CreateContextOldWay()->CGImage?
            {
                func heatMapCGImage()->CGImage?
                {
                    let tempBuffer = malloc(BitmapMemorySize)
                    memcpy(tempBuffer, &dataReference, BytesPerRow * Bitmapsize.height)
                    defer
                    {
                        free(tempBuffer)
                    }
                    let rgbColorSpace:CGColorSpace = CGColorSpaceCreateDeviceRGB()
                    let alphabitmapinfo = CGImageAlphaInfo.premultipliedLast.rawValue
                    if let contextlayer:CGContext = CGContext(data: tempBuffer, width: Bitmapsize.width, height: Bitmapsize.height, bitsPerComponent: 8, bytesPerRow: BytesPerRow, space: rgbColorSpace, bitmapInfo: alphabitmapinfo)
                    {
                        return contextlayer.makeImage()
                    }
                    return nil
                }
                
                if let cgimage = heatMapCGImage()
                {
                    let cgsize:CGSize = CGSize(width: Bitmapsize.width, height: Bitmapsize.height)
                    UIGraphicsBeginImageContext(cgsize)
                    if let contexts = UIGraphicsGetCurrentContext()
                    {
                        let rect = CGRect(origin: CGPoint.zero, size: cgsize)
                        contexts.draw(cgimage, in: rect)
                        return contexts.makeImage()
                    }
                }
                print("Create fail")
                return nil
            }
            let img = CreateContextOldWay()
            UIGraphicsEndImageContext()
            return img
        }
        if let tempimage = getHeatMapContextImage()
        {
            let mapCGRect = rect(for: overlay.boundingMapRect)
            Lastimage = tempimage
            context.clear(mapCGRect)
            self.dataReference.removeAll()
            context.draw(Lastimage!, in: mapCGRect)
        }
        else{
            print("cgcontext error")
        }
    }
複製程式碼

寫到最後發現自己的演演算法有點凌亂,寫這篇文章也是希望能有人能參與這個reop,改進整個效能,整個過程濃縮就是 MKOverlay -> CGImage。

相關文章