阅读 801

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

关注下面的标签,发现更多相似文章
评论
说说你的看法