Gradient Polyline with MapKit ios
One of the idea I came up is to create a CGPath and stroke it with gradient every time when drawMapRect
method been called, since the MKPolylineView
is replaced by MKPlolylineRenderer
in ios7.
I tried to implement this by subclassing a MKOverlayPathRenderer
but I failed to pick out individual CGPath, then I find a mysterious method named-(void) strokePath:(CGPathRef)path inContext:(CGContextRef)context
which sounds like what I need, but it will not be called if you don't call the super method when you override your drawMapRect
.
thats what Im working out for now.
I'll keep trying so if I work out something I'll come back and update the answer.
=========UPDATE================================================
So that is what I'm worked out these days, I almost implemented the basic idea mentioned above but yes, I still cannot pick out an individual PATH according to specific mapRect, so I just draw all paths with gradient at the same time when the boundingBox of all paths intersects with current mapRect. poor trick, but work for now.
In the -(void) drawMapRect:(MKMapRect)mapRect zoomScale:(MKZoomScale)zoomScale inContext:(CGContextRef)context
method in render class, I do this:
CGMutablePathRef fullPath = CGPathCreateMutable();BOOL pathIsEmpty = YES;//merging all the points as entire pathfor (int i=0;i< polyline.pointCount;i++){ CGPoint point = [self pointForMapPoint:polyline.points[i]]; if (pathIsEmpty){ CGPathMoveToPoint(fullPath, nil, point.x, point.y); pathIsEmpty = NO; } else { CGPathAddLineToPoint(fullPath, nil, point.x, point.y); }}//get bounding box out of entire path.CGRect pointsRect = CGPathGetBoundingBox(fullPath);CGRect mapRectCG = [self rectForMapRect:mapRect];//stop any drawing logic, cuz there is no path in current rect.if (!CGRectIntersectsRect(pointsRect, mapRectCG))return;
Then I split the entire path point by point to draw its gradient individually.note that the hues
array containing hue value mapping each velocity of location.
for (int i=0;i< polyline.pointCount;i++){ CGMutablePathRef path = CGPathCreateMutable(); CGPoint point = [self pointForMapPoint:polyline.points[i]]; ccolor = [UIColor colorWithHue:hues[i] saturation:1.0f brightness:1.0f alpha:1.0f]; if (i==0){ CGPathMoveToPoint(path, nil, point.x, point.y); } else { CGPoint prevPoint = [self pointForMapPoint:polyline.points[i-1]]; CGPathMoveToPoint(path, nil, prevPoint.x, prevPoint.y); CGPathAddLineToPoint(path, nil, point.x, point.y); CGFloat pc_r,pc_g,pc_b,pc_a, cc_r,cc_g,cc_b,cc_a; [pcolor getRed:&pc_r green:&pc_g blue:&pc_b alpha:&pc_a]; [ccolor getRed:&cc_r green:&cc_g blue:&cc_b alpha:&cc_a]; CGFloat gradientColors[8] = {pc_r,pc_g,pc_b,pc_a, cc_r,cc_g,cc_b,cc_a}; CGFloat gradientLocation[2] = {0,1}; CGContextSaveGState(context); CGFloat lineWidth = CGContextConvertSizeToUserSpace(context, (CGSize){self.lineWidth,self.lineWidth}).width; CGPathRef pathToFill = CGPathCreateCopyByStrokingPath(path, NULL, lineWidth, self.lineCap, self.lineJoin, self.miterLimit); CGContextAddPath(context, pathToFill); CGContextClip(context);//<--clip your context after you SAVE it, important! CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB(); CGGradientRef gradient = CGGradientCreateWithColorComponents(colorSpace, gradientColors, gradientLocation, 2); CGColorSpaceRelease(colorSpace); CGPoint gradientStart = prevPoint; CGPoint gradientEnd = point; CGContextDrawLinearGradient(context, gradient, gradientStart, gradientEnd, kCGGradientDrawsAfterEndLocation); CGGradientRelease(gradient); CGContextRestoreGState(context);//<--Don't forget to restore your context. } pcolor = [UIColor colorWithCGColor:ccolor.CGColor];}
That is all the core drawing method and of course you need points
, velocity
in your overlay class and feed them with CLLocationManager.
the last point is how to get hue
value out of velocity, well, I found that if hue ranging from 0.03~0.3 is exactly represent from red to green, so I do some proportionally mapping to hue and velocity.
last of the last, here you are this is full source of this demo:https://github.com/wdanxna/GradientPolyline
don't panic if can't see the line you draw, I just position the map region on my position :)
I implemented a Swift 4 version inspired of @wdanxna solution above. Some thing have changed, the path is already created in the superclass.
Instead of storing the hues in the renderer I made a subclass of MKPolyline that calculates the hues in the constructor. Then I grab the polyline with the values from the renderer. I mapped it to the speed but I guess you can map the gradient to whatever you want.
GradientPolyline
class GradientPolyline: MKPolyline { var hues: [CGFloat]? public func getHue(from index: Int) -> CGColor { return UIColor(hue: (hues?[index])!, saturation: 1, brightness: 1, alpha: 1).cgColor }}extension GradientPolyline { convenience init(locations: [CLLocation]) { let coordinates = locations.map( { $0.coordinate } ) self.init(coordinates: coordinates, count: coordinates.count) let V_MAX: Double = 5.0, V_MIN = 2.0, H_MAX = 0.3, H_MIN = 0.03 hues = locations.map({ let velocity: Double = $0.speed if velocity > V_MAX { return CGFloat(H_MAX) } if V_MIN <= velocity || velocity <= V_MAX { return CGFloat((H_MAX + ((velocity - V_MIN) * (H_MAX - H_MIN)) / (V_MAX - V_MIN))) } if velocity < V_MIN { return CGFloat(H_MIN) } return CGFloat(velocity) }) }}
GradidentPolylineRenderer
class GradidentPolylineRenderer: MKPolylineRenderer { override func draw(_ mapRect: MKMapRect, zoomScale: MKZoomScale, in context: CGContext) { let boundingBox = self.path.boundingBox let mapRectCG = rect(for: mapRect) if(!mapRectCG.intersects(boundingBox)) { return } var prevColor: CGColor? var currentColor: CGColor? guard let polyLine = self.polyline as? GradientPolyline else { return } for index in 0...self.polyline.pointCount - 1{ let point = self.point(for: self.polyline.points()[index]) let path = CGMutablePath() currentColor = polyLine.getHue(from: index) if index == 0 { path.move(to: point) } else { let prevPoint = self.point(for: self.polyline.points()[index - 1]) path.move(to: prevPoint) path.addLine(to: point) let colors = [prevColor!, currentColor!] as CFArray let baseWidth = self.lineWidth / zoomScale context.saveGState() context.addPath(path) let gradient = CGGradient(colorsSpace: nil, colors: colors, locations: [0, 1]) context.setLineWidth(baseWidth) context.replacePathWithStrokedPath() context.clip() context.drawLinearGradient(gradient!, start: prevPoint, end: point, options: []) context.restoreGState() } prevColor = currentColor } }}
How to use
Create a line from an array of CLLocations
let runRoute = GradientPolyline(locations: locations)self.mapView.addOverlay(runRoute)
Pass the GradientPolylineRenderer in the delegate
func mapView(_ mapView: MKMapView, rendererFor overlay: MKOverlay) -> MKOverlayRenderer { if overlay is GradientPolyline { let polyLineRender = GradientMKPolylineRenderer(overlay: overlay) polyLineRender.lineWidth = 7 return polyLineRender }}
Result