本教程使用Swift 3.1, Xcode 8.0
代碼:https://github.com/jamesdouble/JDSwiftHeatMap
現(xiàn)在Iphone使用者常使用的地圖插件,不外乎就是高德與百度,國(guó)外則是Google,看來看去就是沒啥人在用本地端自帶的MKMapView,一個(gè)原因是起步晚所以欠缺很多使用者經(jīng)驗(yàn)跟資料,再來一個(gè)我自己認(rèn)為是現(xiàn)成API極少,MKMapView基本上只有Annotaion,Overlay是Developer可以自訂的,而百度有軌跡,雷達(dá)...等已經(jīng)是現(xiàn)成的API。
于是我越想越不順心,要用還是要用咱IOS原生自帶的,在網(wǎng)上搜了一圈只看到一個(gè)用OC寫的古老項(xiàng)目,用起來總不順心,現(xiàn)在想經(jīng)由開源的方法匯整大家意見來提高整體的自由度跟使用性。
熱度圖是早期(1991)就已經(jīng)出現(xiàn)的資料表達(dá)形式(矩陣表示),其成熟度以及相對(duì)應(yīng)衍生圖像也是相對(duì)于其他的地圖表達(dá)方式成熟。熱度圖
前言
實(shí)作起來不需要用太廣的知識(shí)或是什么深不見底的技術(shù),基本上只要熟悉兩個(gè)區(qū)塊:
MapKit : 這個(gè)當(dāng)然是必須的,畢竟我們是要建立在原生的地圖上,但基本的如何新增Overlay,OverlayRender...等,這篇文章不會(huì)做太多解釋。
CGContext : 也就是指Core Graphic, 這塊應(yīng)該是不管走到哪都會(huì)碰到的冤家,不外乎就是涂鴉著色啦~
使用者Input
利用Delegate取得資料點(diǎn)的經(jīng)緯度、影響范圍跟影響力。
HeatMap on MapKit - 記錄位置
MapKit該做的就是MapKit“能”做的,記錄相關(guān)的地理資料,包括資料的“經(jīng)緯度座標(biāo)“以及距離。
-
MKOVeraly:很明顯,熱度圖這樣超級(jí)不規(guī)則的圖形,MKCircle,MKPolyline,MKPolygon...等,并不能滿足我們需要的,還是得從最根本的MKOverlay重新創(chuàng)造一個(gè)子類別。
JDHeatOverlay- 計(jì)算Overlay的BoudingMapRect(涵蓋范圍):
/** 有新的點(diǎn)加進(jìn)來 -> 重新計(jì)算這個(gè)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{ //非首次計(jì)算 -> 把上次計(jì)算的MapRect拿出來,比MaxX,Y MinX,Y MaxX = MKMapRectGetMaxX(BeenCaculatedMapRect) let heatmaprect = newPoint.MapRect let tMaxX = MKMapRectGetMaxX(heatmaprect) MaxX = (tMaxX > MaxX) ? tMaxX : MaxX . . //每次計(jì)算新的資料點(diǎn),MapRect都會(huì)變大。} else{ //首次計(jì)算 -> 取第一個(gè)點(diǎn)的Maprecr let heatmaprect = newPoint.MapRect . . } let rect = MKMapRectMake(MinX, MinY, MaxX - MinX, MaxY - MinY) self.CaculatedMapRect = rect }
-
同理,現(xiàn)有的OverlayRender都無法滿足,我們要的形狀,所以也是重新定義一個(gè)類別。
JDHeatOverlayRender- draw是這個(gè)類最重要的Func,再之后Core Graphic 那段一起寫。
override func draw(_ mapRect: MKMapRect, zoomScale: MKZoomScale, in context: CGContext)
過渡(MapKit -> Core Graphic)
熟悉MapKit的朋友們一定都知道,MKMapRect與CGRect的差別,也清楚他的轉(zhuǎn)換方法,通常只會(huì)在上述的" draw ",也就是要畫的時(shí)候進(jìn)行轉(zhuǎn)換,但我這邊必須提早進(jìn)行,因?yàn)槲冶仨毾戎牢乙嬍裁矗晕疫@里自帶一個(gè)名詞[ RowFormData ]。
-
使用者資料叢集轉(zhuǎn)換前:
單位:MKMapRect,位置:MKMapPoint,范圍:KilloMeter,原點(diǎn):很大
-
使用者資料轉(zhuǎn)換后:
單位:CGRect,位置:CGPoint,范圍:CGFloat,原點(diǎn):(0,0)
//JDOverlayRender
override func caculateRowFormData(maxHeat level:Int)->(data:[RowFormHeatData],rect:CGRect)?
{
var rowformArr:[RowFormHeatData] = []
//
for heatpoint in overlay.HeatPointsArray
{
//將整個(gè)叢集轉(zhuǎn)換成CGRect
let mkmappoint = heatpoint.MidMapPoint
let GlobalCGpoint:CGPoint = self.point(for: mkmappoint)
let OverlayCGRect = rect(for: overlay.boundingMapRect)
//將原點(diǎn)化成(0,0)
let localX = GlobalCGpoint.x - (OverlayCGRect.origin.x)
let localY = GlobalCGpoint.y - (OverlayCGRect.origin.y)
let loaclCGPoint = CGPoint(x: localX, y: localY)
//將半徑轉(zhuǎn)乘CGFloat
let radiusinMKDistanse:Double = heatpoint.radiusInMKDistance
let radiusmaprect = MKMapRect(origin: MKMapPoint.init(), size: MKMapSize(width: radiusinMKDistanse, height: radiusinMKDistanse))
let radiusCGDistance = rect(for: radiusmaprect).width
//儲(chǔ)存新的資料集
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)
}
```
計(jì)算層:將RowFormData->CGImage
我們有了RowFormData后,就能開始計(jì)算什么位置放什么顏色,我們這里自創(chuàng)一個(gè)簡(jiǎn)易的類別,來幫助我們區(qū)隔該做的事:
這邊會(huì)用到的Core Graphic并不是一般常見的UIGraphicsBeginImageContext之后,GetContext在做movePoint,addArc,addPath....等,因?yàn)橐俅螐?qiáng)調(diào)我們圖層的形狀是超級(jí)不規(guī)則,甚至還要計(jì)算顏色。
超級(jí)踩坑區(qū)
超級(jí)踩坑區(qū)
超級(jí)踩坑區(qū)
我們要用的是CGContex里的建構(gòu)式
參數(shù)有data,width,height,bitsPerComponent,bytesPerRow,space,bitmapInfo
該怎么看呢?
(對(duì)于圖片概念不熟悉的朋友,我在這也扯不完,網(wǎng)上搜索Bitmap或Pixels還有RGB應(yīng)該就很多了。)
Color Bitmap http://jbrd.github.io/2008/02/01/bitmap-and-indexed-images.html參數(shù)只要配對(duì)錯(cuò)誤就會(huì)報(bào)錯(cuò),而且不會(huì)跟你說錯(cuò)哪
上圖的width,height已經(jīng)有了,就是剛剛計(jì)算出來的CGRect
CGColorSpace & BitmapInfo:這兩個(gè)參數(shù)相輔相成,就是告訴它你的data會(huì)以什么樣的形式呈現(xiàn),以RGB或是灰階...等,上面的圖片是RGB,我們要用的也是RGB(space = CGColorSpaceCreateDeviceRGB()),但是多了一個(gè)值A(chǔ)lpha這個(gè)值大家,bitmapInfo = CGImageAlphaInfo.premultipliedLast.rawValue,這告訴它alpha直放在最后 -->
也就是一個(gè)Pixel格式(R G B A)
有了Pixel格式就知道它的大小,四個(gè)值都是0~255所以是8個(gè)Bits(BitsPerComponent),一個(gè)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)
//計(jì)算每個(gè)資料點(diǎn)對(duì)這個(gè)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個(gè)Byte進(jìn)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")
}
}
寫到最后發(fā)現(xiàn)自己的演算法有點(diǎn)凌亂,寫這篇文章也是希望能有人能參與這個(gè)reop,改進(jìn)整個(gè)效能,整個(gè)過程濃縮就是 MKOverlay -> CGImage。