I\'m trying to trace a route on a MKMapView using overlays (MKOverlay). However, depending on the current speed, I want to do something like the Nike app with a gradient whi
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
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 path
for (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 :)
sounds to me that your drawRect in the line drawing view is missing the setting of the gradient. drawing might occur on a different thread for the overlays. please post the code.