如何实作原生IOS热度图-CG直接做图+MapKit

本教程使用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(涵盖范围):
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    /**
    有新的点加进来 ->
    重新计算这个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 那段一起写。
    1
    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)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    //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里的建构式

参数有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)阵列。

回到代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
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。

Comments

Your browser is out-of-date!

Update your browser to view this website correctly.&npsb;Update my browser now

×