本教程使用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(涵盖范围):
/** 有新的点加进来 -> 重新计算这个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 那段一起写。
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后,就能开始计算什么位置放什么颜色,我们这里自创一个简易的类别,来帮助我们区隔该做的事:
这边会用到的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)阵列。
回到代码:
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。