本教程使用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。