本教程使用Swift 3.1, Xcode 8.0html
代碼:https://github.com/jamesdouble/JDSwiftHeatMapgit
如今Iphone使用者常使用的地圖插件,不外乎就是高德與百度,國外則是Google,看來看去就是沒啥人在用本地端自帶的MKMapView,一個緣由是起步晚因此欠缺不少使用者經驗跟資料,再來一個我本身認爲是現成API極少,MKMapView基本上只有Annotaion,Overlay是Developer能夠自訂的,而百度有軌跡,雷達...等已是現成的API。github
因而我越想越不順心,要用仍是要用咱IOS原生自帶的,在網上搜了一圈只看到一個用OC寫的古老項目,用起來總不順心,如今想經由開源的方法彙整你們意見來提升總體的自由度跟使用性。算法
熱度圖
熱度圖是早期(1991)就已經出現的資料表達形式(矩陣表示),其成熟度以及相對應衍生圖像也是相對於其餘的地圖表達方式成熟。api
實做起來不須要用太廣的知識或是什麼深不見底的技術,基本上只要熟悉兩個區塊:app
MapKit : 這個固然是必須的,畢竟咱們是要創建在原生的地圖上,但基本的如何新增Overlay,OverlayRender...等,這篇文章不會作太多解釋。ide
CGContext : 也就是指***Core Graphic***, 這塊應該是無論走到哪都會碰到的冤家,不外乎就是塗鴉着色啦~ui
利用Delegate取得資料點的經緯度、影響範圍跟影響力。spa
MapKit該作的就是MapKit「能」作的,記錄相關的地理資料,包括資料的「經緯度座標「以及距離。插件
MKOVeraly:很明顯,熱度圖這樣超級不規則的圖形,MKCircle,MKPolyline,MKPolygon...等,並不能知足咱們須要的,仍是得從最根本的MKOverlay從新創造一個子類別。
/** 有新的點加進來 -> 從新計算這個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都沒法知足,咱們要的形狀,因此也是從新定義一個類別。
override func draw(_ mapRect: MKMapRect, zoomScale: MKZoomScale, in context: CGContext)
複製代碼
熟悉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後,就能開始計算什麼位置放什麼顏色,咱們這裏自創一個簡易的類別,來幫助咱們區隔該作的事:
這邊會用到的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
}
}
}
複製代碼
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。