Gradient Polyline with MapKit ios
Asked Answered
D

5

23

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 while tracing the route, if the color is changing (for example, from green to orange if a user is driving from 65mph to 30mph).

Here's a screenshot of what I want:

gradient overlay image

So every 20 meters, I adding an overlay from the old to the new coordinates using:

// Create a c array of points. 
MKMapPoint *pointsArray = malloc(sizeof(CLLocationCoordinate2D) * 2);

// Create 2 points.
MKMapPoint startPoint = MKMapPointForCoordinate(CLLocationCoordinate2DMake(oldLatitude, oldLongitude));
MKMapPoint endPoint = MKMapPointForCoordinate(CLLocationCoordinate2DMake(newLatitude, newLongitude));

// Fill the array.
pointsArray[0] = startPoint;
pointsArray[1] = endPoint;

// Erase polyline and polyline view if not nil.
if (self.routeLine != nil)
    self.routeLine = nil;

if (self.routeLineView != nil)
    self.routeLineView = nil;

// Create the polyline based on the array of points.
self.routeLine = [MKPolyline polylineWithPoints:pointsArray count:2];

// Add overlay to map.
[self.mapView addOverlay:self.routeLine];

// clear the memory allocated earlier for the points.
free(pointsArray);

// Save old coordinates.
oldLatitude = newLatitude;
oldLongitude = newLongitude;

Basically I'm adding a lot of little overlays. Then I would like to create the gradient on this little line drawing, so I'm trying to do so in the overlay delegate:

- (MKOverlayView *)mapView:(MKMapView *)mapView viewForOverlay:(id <MKOverlay>)overlay {
    MKOverlayView* overlayView = nil;

    if(overlay == self.routeLine) {
        // If we have not yet created an overlay view for this overlay, create it now. 
        if(self.routeLineView == nil) {
            self.routeLineView = [[[MKPolylineView alloc] initWithPolyline:self.routeLine] autorelease];

            if (speedMPH < 25.0) {
                self.routeLineView.fillColor = [UIColor redColor];
                self.routeLineView.strokeColor = [UIColor redColor];
            }
            else if (speedMPH >= 25.0 && speedMPH < 50.0) {
                self.routeLineView.fillColor = [UIColor orangeColor];
                self.routeLineView.strokeColor = [UIColor orangeColor];
            }
            else {
                self.routeLineView.fillColor = [UIColor greenColor];
                self.routeLineView.strokeColor = [UIColor greenColor];
            }

            // Size of the trace.
            self.routeLineView.lineWidth = routeLineWidth;

            // Add gradient if color changed.
            if (oldColor != self.routeLineView.fillColor) {
                CAGradientLayer *gradient = [CAGradientLayer layer];
                    gradient.frame = self.routeLineView.bounds;
                    gradient.colors = [NSArray arrayWithObjects:(id)[oldColor CGColor], (id)[self.routeLineView.fillColor CGColor], nil];
                    [self.routeLineView.layer insertSublayer:gradient atIndex:0];
            }

            // Record old color for gradient.
            if (speedMPH < 25.0)
                oldColor = [UIColor redColor];
            else if (speedMPH >= 25.0 && speedMPH < 50.0)
                oldColor = [UIColor orangeColor];
            else
                oldColor = [UIColor greenColor];
        }

        overlayView = self.routeLineView;
    }

    return overlayView;
}

I'm trying to add the gradient this way, but I guess it is not the way to do it because I can't make it to work.

I can also trace the route every time there is an update on the user's location (in the location object's delegate), or as above every 20 meters.

Can you please help me on that one, giving me tips! Thanks!

Denbrook answered 15/4, 2011 at 21:48 Comment(2)
How are routeLine and routeLineView declared? What class is the above code in? "Can't make it work" - what happens?Pallbearer
I just don't see the gradients that I'm adding to my overlay view. MKPolyline *routeLine; and MKPolylineView *routeLineView;, then every 20 meters I create a new one and add it as overlays. And in the MKMapView delegate function above I create the view depending on criterias and try to add a gradient to it.Denbrook
F
16

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================================================

enter image description here

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 :)

Feriga answered 23/11, 2013 at 6:29 Comment(3)
Just saw your answer. I think this is exactly what I need to achieve in this question : #20247813 . Basically, if I understand correctly, you recreate the path manually segment by segment and apply a gradient only to the currently added one by clipping each time. Is this optimal/fast enough ?Marasco
At the very first, I tried to draw entire path with one gradient object which has all color values I need in gradientColors and each color value mapping one location value in my gradientLocation. I thought that should work but its not(weird). and document says we should not draw any thing out of current MapRect, its seems like drawing entire path once for all is not an option? so I think split it into segment is the easiest way... talking about optimal, its running ok in my iPhone :) but there still has something to improve, like: only draw lines that intersects with current MapRect.Feriga
I implemented my stuff based on your code and github example and works absolutely great. Only changes I'd suggest is to prepare the path directly in the init and just use in the drawRect or simply use CGRect pointsR = [self rectForMapRect:polyline.boundingMapRect]; and check also for CGRectContains, not only for intersection. My question is still unanswered and has a bounty. You could post your answer above there and I'd accept it happily, it helped me a lot :)Marasco
C
13

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

Gradient Polyline

Camphorate answered 2/11, 2018 at 22:25 Comment(0)
H
1

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.

Horseshit answered 21/6, 2011 at 10:35 Comment(9)
Please see the code above in my post, I updated it. I have one function that adds overlays from point A to point B every 20 meters, and then I'm trying to add the gradient layer when I create the overlay view in the map view delegate.Denbrook
Your logic seems to be wrong. You cannot add gradient layers to the overlay like this. Instead you have to have a method to build and draw the entire path with the individual segments of different shades. There is WWDC 2010 sample code ("Breadcrumbs") demonstrating this. Your current approach cannot work because you are only adding path segments as new sublayers on the very first time. But this will cause the layer tree to become extremely large over time. You need to be able to draw the entire path in one layer.Horseshit
Ok, thanks @Horseshit I will take a look at it and keep you updated!Denbrook
I can't find the sample code, I tried to look for it on Apple's website, etc., and also can't log in to the WWDC because I haven't buy a ticket. Any way you could provide me this ressource? Thanks!Denbrook
Go to the WWDC 2010 video site in iTunes via developer.apple.com/itunes/… - then on the bottom right side under LINKS there is a download link for all the sample source code.Horseshit
Awesome, it's downloading. ThanksDenbrook
So I implemented the overlay like on the breadcrumbs sample code, it works great. It is drawing the entire path in blue with .5 alpha, and it is where I'd like to change colors depending on a value (speed for example), but I don't know where to do it. I feel like it would be when I create the path in - (CGPathRef)createPathForPoints:pointCount:clipRect:zoomScale: function, do you agree with that?Denbrook
the problem here is that one path can only have one color. You essentially have to have an array of paths, each with their own color.Horseshit
having multiple (for various speeds) mkoverlays & corresponding views, is costly. The mapkit gets very busy adding those. So it's kind of hard to get a smooth look.Rheology
T
1

Since iOS 14 MKGradientPolyline is probably the best option. It also works with strokeStart and strokeEnd so you can animate the path, which previous solutions don't currently support.

Tin answered 2/6, 2022 at 14:44 Comment(1)
Best answer on above question from iOS 14 onwardsClipfed
F
0

gugge’s code is very useful. But it's better to change it like this.

return CGFloat((H_MIN + ((velocity - V_MIN) * (H_MAX - H_MIN)) / (V_MAX - V_MIN)))
Fennec answered 29/12, 2021 at 9:20 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.