如何实作原生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。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342

推荐阅读更多精彩内容