如何实作原生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)就已经出现的资料表达形式(矩阵表示),其成熟度以及相对应衍生图像也是相对于其他的地图表达方式成熟。
前言
实作起来不需要用太广的知识或是什么深不见底的技术,基本上只要熟悉两个区块:
MapKit : 这个当然是必须的,毕竟我们是要建立在原生的地图上,但基本的如何新增Overlay,OverlayRender…等,这篇文章不会做太多解释。
CGContext : 也就是指Core Graphic, 这块应该是不管走到哪都会碰到的冤家,不外乎就是涂鸦着色啦~
使用者Input
利用Delegate取得资料点的经纬度、影响范围跟影响力。
HeatMap on MapKit - 记录位置
MapKit该做的就是MapKit“能”做的,记录相关的地理资料,包括资料的“经纬度座标“以及距离。
MKOVeraly:很明显,热度图这样超级不规则的图形,MKCircle,MKPolyline,MKPolygon…等,并不能满足我们需要的,还是得从最根本的MKOverlay重新创造一个子类别。
- 计算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
}同理,现有的OverlayRender都无法满足,我们要的形状,所以也是重新定义一个类别。
- 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后,就能开始计算什么位置放什么颜色,我们这里自创一个简易的类别,来帮助我们区隔该做的事:
这边会用到的Core Graphic并不是一般常见的UIGraphicsBeginImageContext之后,GetContext在做movePoint,addArc,addPath….等,因为要再次强调我们图层的形状是超级不规则,甚至还要计算颜色。
超级踩坑区
超级踩坑区
超级踩坑区
我们要用的是CGContex里的建构式
参数有data,width,height,bitsPerComponent,bytesPerRow,space,bitmapInfo
该怎么看呢?
(对于图片概念不熟悉的朋友,我在这也扯不完,网上搜索Bitmap或Pixels还有RGB应该就很多了。)
参数只要配对错误就会报错,而且不会跟你说错哪
上图的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 | override func produceRowData() |
有了Data回到Render
1 | override func draw(_ mapRect: MKMapRect, zoomScale: MKZoomScale, in context: CGContext) { |
写到最后发现自己的演算法有点凌乱,写这篇文章也是希望能有人能参与这个reop,改进整个效能,整个过程浓缩就是 MKOverlay -> CGImage。