如何實(shí)作原生IOS熱度圖 CG直接做圖+MapKit - 從0到Double系列

本教程使用Swift 3.1, Xcode 8.0

代碼:https://github.com/jamesdouble/JDSwiftHeatMap


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

于是我越想越不順心,要用還是要用咱IOS原生自帶的,在網(wǎng)上搜了一圈只看到一個(gè)用OC寫的古老項(xiàng)目,用起來總不順心,現(xiàn)在想經(jīng)由開源的方法匯整大家意見來提高整體的自由度跟使用性。

熱度圖

熱度圖是早期(1991)就已經(jīng)出現(xiàn)的資料表達(dá)形式(矩陣表示),其成熟度以及相對(duì)應(yīng)衍生圖像也是相對(duì)于其他的地圖表達(dá)方式成熟。
熱度圖種類 - source:WIKI

前言

實(shí)作起來不需要用太廣的知識(shí)或是什么深不見底的技術(shù),基本上只要熟悉兩個(gè)區(qū)塊:

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

  2. CGContext : 也就是指Core Graphic, 這塊應(yīng)該是不管走到哪都會(huì)碰到的冤家,不外乎就是涂鴉著色啦~

使用者Input

利用Delegate取得資料點(diǎn)的經(jīng)緯度、影響范圍跟影響力。

HeatMap on MapKit - 記錄位置

MapKit該做的就是MapKit“能”做的,記錄相關(guān)的地理資料,包括資料的“經(jīng)緯度座標(biāo)“以及距離。

  1. MKOVeraly:很明顯,熱度圖這樣超級(jí)不規(guī)則的圖形,MKCircle,MKPolyline,MKPolygon...等,并不能滿足我們需要的,還是得從最根本的MKOverlay重新創(chuàng)造一個(gè)子類別。


    JDHeatOverlay
    • 計(jì)算Overlay的BoudingMapRect(涵蓋范圍):
    /**
     有新的點(diǎn)加進(jìn)來 ->
     重新計(jì)算這個(gè)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{
            //非首次計(jì)算 -> 把上次計(jì)算的MapRect拿出來,比MaxX,Y MinX,Y
            MaxX = MKMapRectGetMaxX(BeenCaculatedMapRect)
            let heatmaprect = newPoint.MapRect
            let tMaxX = MKMapRectGetMaxX(heatmaprect)
            MaxX = (tMaxX > MaxX) ? tMaxX : MaxX
            .
            .
            //每次計(jì)算新的資料點(diǎn),MapRect都會(huì)變大。}
        else{
            //首次計(jì)算 -> 取第一個(gè)點(diǎn)的Maprecr
            let heatmaprect = newPoint.MapRect
            .
            .        }
        let rect = MKMapRectMake(MinX, MinY, MaxX - MinX, MaxY - MinY)
        self.CaculatedMapRect = rect
    }
    
  2. 同理,現(xiàn)有的OverlayRender都無法滿足,我們要的形狀,所以也是重新定義一個(gè)類別。

    JDHeatOverlayRender
    • draw是這個(gè)類最重要的Func,再之后Core Graphic 那段一起寫。
    override func draw(_ mapRect: MKMapRect, zoomScale: MKZoomScale, in context: CGContext)
    

過渡(MapKit -> Core Graphic)

熟悉MapKit的朋友們一定都知道,MKMapRect與CGRect的差別,也清楚他的轉(zhuǎn)換方法,通常只會(huì)在上述的" draw ",也就是要畫的時(shí)候進(jìn)行轉(zhuǎn)換,但我這邊必須提早進(jìn)行,因?yàn)槲冶仨毾戎牢乙嬍裁矗晕疫@里自帶一個(gè)名詞[ RowFormData ]

過度過程
  • 使用者資料叢集轉(zhuǎn)換前:

    單位:MKMapRect,位置:MKMapPoint,范圍:KilloMeter,原點(diǎn):很大

  • 使用者資料轉(zhuǎn)換后:

    單位:CGRect,位置:CGPoint,范圍:CGFloat,原點(diǎn):(0,0)

//JDOverlayRender
override func caculateRowFormData(maxHeat level:Int)->(data:[RowFormHeatData],rect:CGRect)?
{
var rowformArr:[RowFormHeatData] = []
//
for heatpoint in overlay.HeatPointsArray
{
//將整個(gè)叢集轉(zhuǎn)換成CGRect
let mkmappoint = heatpoint.MidMapPoint
let GlobalCGpoint:CGPoint = self.point(for: mkmappoint)
let OverlayCGRect = rect(for: overlay.boundingMapRect)
//將原點(diǎn)化成(0,0)
let localX = GlobalCGpoint.x - (OverlayCGRect.origin.x)
let localY = GlobalCGpoint.y - (OverlayCGRect.origin.y)
let loaclCGPoint = CGPoint(x: localX, y: localY)
//將半徑轉(zhuǎn)乘CGFloat
let radiusinMKDistanse:Double = heatpoint.radiusInMKDistance
let radiusmaprect = MKMapRect(origin: MKMapPoint.init(), size: MKMapSize(width: radiusinMKDistanse, height: radiusinMKDistanse))
let radiusCGDistance = rect(for: radiusmaprect).width
//儲(chǔ)存新的資料集
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)
}
```

計(jì)算層:將RowFormData->CGImage

我們有了RowFormData后,就能開始計(jì)算什么位置放什么顏色,我們這里自創(chuàng)一個(gè)簡(jiǎn)易的類別,來幫助我們區(qū)隔該做的事:


RowDataProducer

這邊會(huì)用到的Core Graphic并不是一般常見的UIGraphicsBeginImageContext之后,GetContext在做movePoint,addArc,addPath....等,因?yàn)橐俅螐?qiáng)調(diào)我們圖層的形狀是超級(jí)不規(guī)則,甚至還要計(jì)算顏色。

超級(jí)踩坑區(qū)

超級(jí)踩坑區(qū)

超級(jí)踩坑區(qū)

我們要用的是CGContex里的建構(gòu)式


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

參數(shù)有data,width,height,bitsPerComponent,bytesPerRow,space,bitmapInfo
該怎么看呢?
(對(duì)于圖片概念不熟悉的朋友,我在這也扯不完,網(wǎng)上搜索Bitmap或Pixels還有RGB應(yīng)該就很多了。)

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

參數(shù)只要配對(duì)錯(cuò)誤就會(huì)報(bào)錯(cuò),而且不會(huì)跟你說錯(cuò)哪

  • 上圖的width,height已經(jīng)有了,就是剛剛計(jì)算出來的CGRect

  • CGColorSpace & BitmapInfo:這兩個(gè)參數(shù)相輔相成,就是告訴它你的data會(huì)以什么樣的形式呈現(xiàn),以RGB或是灰階...等,上面的圖片是RGB,我們要用的也是RGB(space = CGColorSpaceCreateDeviceRGB()),但是多了一個(gè)值A(chǔ)lpha這個(gè)值大家,bitmapInfo = CGImageAlphaInfo.premultipliedLast.rawValue,這告訴它alpha直放在最后 -->

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

  • 有了Pixel格式就知道它的大小,四個(gè)值都是0~255所以是8個(gè)Bits(BitsPerComponent),一個(gè)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)
                    //計(jì)算每個(gè)資料點(diǎn)對(duì)這個(gè)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個(gè)Byte進(jìn)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")
        }
    }

寫到最后發(fā)現(xiàn)自己的演算法有點(diǎn)凌亂,寫這篇文章也是希望能有人能參與這個(gè)reop,改進(jìn)整個(gè)效能,整個(gè)過程濃縮就是 MKOverlay -> CGImage。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,362評(píng)論 6 537
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 99,013評(píng)論 3 423
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,346評(píng)論 0 382
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我,道長(zhǎng),這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,421評(píng)論 1 316
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 72,146評(píng)論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,534評(píng)論 1 325
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,585評(píng)論 3 444
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,767評(píng)論 0 289
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,318評(píng)論 1 335
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 41,074評(píng)論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 43,258評(píng)論 1 371
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,828評(píng)論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,486評(píng)論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,916評(píng)論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,156評(píng)論 1 290
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 51,993評(píng)論 3 395
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 48,234評(píng)論 2 375

推薦閱讀更多精彩內(nèi)容