-->

MKPolylineRenderer produces jagged, unequal paths

2020-02-12 04:05发布

问题:

I am using the iOS 7 MapKit APIs to produce 3D camera movements on a map that displays an MKDirectionsRequest-produced path. The path is rendered by MKOverlayRenderer like so:

-(void)showRoute:(MKDirectionsResponse *)response
{
for (MKRoute *route in response.routes)
 {
    [self.map
     addOverlay:route.polyline level:MKOverlayLevelAboveRoads];
 }
}

- (MKOverlayRenderer *)mapView:(MKMapView *)mapView rendererForOverlay:(id < MKOverlay >)overlay
{
 MKPolylineRenderer *renderer =
 [[MKPolylineRenderer alloc] initWithOverlay:overlay];
UIColor *mapOverlayColor = [UIColor colorWithRed:((float)22 / 255.0f) green:((float)126 / 255.0f) blue:((float)251 / 255.0f) alpha:0.8];
 renderer.strokeColor = mapOverlayColor;
 renderer.lineWidth = 13.0;
 return renderer;
}

It's working well except for one issue. When I zoom or pan around the path with MKMapCameras (and without them, if I simply do so as the user), the path is jagged as shown in this screenshot:

I tested to see if switching to MKOverlayLevelAboveLabels makes a difference but sadly the outcome was the same.

Does anyone have suggestions as to how to improve the rendering? Does switching to a geodesic path make a difference and if so, how would I implement this here?

回答1:

Subclass MKPolylineRenderer and override applyStrokePropertiesToContext:atZoomScale: so that it ignores the scale, and draws lines at constant width:

@interface ConstantWidthPolylineRenderer : MKPolylineRenderer
@end

@implementation ConstantWidthPolylineRenderer

- (void)applyStrokePropertiesToContext:(CGContextRef)context
                           atZoomScale:(MKZoomScale)zoomScale
{
    [super applyStrokePropertiesToContext:context atZoomScale:zoomScale];
    CGContextSetLineWidth(context, self.lineWidth);
}

@end

Now use it and admire its smooth rendering:

- (MKOverlayRenderer *)mapView:(MKMapView *)mapView rendererForOverlay:(id<MKOverlay>)overlay
{
    MKPolyline *polyline = (MKPolyline *)overlay;
    ConstantWidthPolylineRenderer *renderer = [[ConstantWidthPolylineRenderer alloc] initWithPolyline:polyline];
    renderer.strokeColor = [UIColor redColor];
    renderer.lineWidth = 40;
    return renderer;
}


回答2:

once the line is drawn on the map it may not get re-rendered if the user zooms. Or, if it does, it may get re-rendered before the user finishes zooming. In this case the width after zooming will no longer reflect your desired width in meters. One way to deal with this is to override regionDidChangeAnimated and remove the overlay and add it back.



回答3:

MKPolylineRenderer is seriously broken as it will not redraw offscreen and it has faulty logic for calculating its cliprect which causes endcap artifacts to be left on the screen. Removing and readding the overlay did nothing for me. Trying to fix the line width does work but you will still get endcap problems with larger line widths. Using the roadSizeForZoomLevel option wont work either (lineWidth = 0)

To get rid of the endcap artifacts that never go away I used the renderer from the Breadcrumb sample app. Now I just have the occasionally unacceptable issue of the redrawing when moving the map around.

The breadrumb renderer I think is what the PolylineRenderer was supposed to be but someone broke it. But even still its not clear how one would force offscreen redraws (Im not a core graphics expert but given that apple maps app doesnt exhibit this behavior Im sure a guru could figure it out.

Anyway if you at least want a renderer that wont leave junk on the screen use the Breadcrumb renderer. Thats the best I could find. If you really need a better mapkit try googmaps



回答4:

MKPolyline is not drawing when zoom is changing and when the region is changing. Simple fix below.

public class PolylineRenderer : MKPolylineRenderer {

    private var displayLink: CADisplayLink!
    private var ticks = 0

    override public init(overlay: MKOverlay) {
        super.init(overlay: overlay)

        displayLink = CADisplayLink(target: self, selector:#selector(PolylineRenderer._update))
    displayLink.add(to: .main, forMode: .commonModes)
    }

    func _update() {
        if ticks < 3 {
            ticks+=1
        } else {
            ticks = 0
        }

        if ticks == 0 {
            self.setNeedsDisplay()
        }
    }

    deinit {
        if displayLink != nil {
            displayLink.invalidate()
        }
    }
}

Its pretty simple once you realize its just not painting fast enough. Skipping 3 ticks does not kill the CPU and bye to the jaggies.



回答5:

Swift 3 solution :

Create a subclass of MKPolylineRenderer

class CustomPolyline: MKPolylineRenderer {

    override func applyStrokeProperties(to context: CGContext, atZoomScale zoomScale: MKZoomScale) {
        super.applyStrokeProperties(to: context, atZoomScale: zoomScale)
        UIGraphicsPushContext(context)
        if let ctx = UIGraphicsGetCurrentContext() {
            ctx.setLineWidth(self.lineWidth)
        }
    }

}

Then use it in your rendererFor MapKit delegate :

func mapView(_ mapView: MKMapView, rendererFor overlay: MKOverlay) -> MKOverlayRenderer {
        let renderer = CustomPolyline(overlay: overlay)
        renderer.strokeColor = UIColor.red
        renderer.lineWidth = 100
        return renderer
}

Your polylines won't re-render after zooming thus avoiding the artifacts



回答6:

OK, I have fixed this problem with slow MKPolylineRenderer rendering to slowly. First use the Breadcrumb renderer from [Apple breadcrumb sample][1]https://developer.apple.com/library/content/samplecode/Breadcrumb/Introduction/Intro.html#//apple_ref/doc/uid/DTS40010048-Intro-DontLinkElementID_2

Simply instead of adding points dynamically to the CrumpPath just add your path.

Secondly, now that you have fixed MKPolylines faulty rendering, you need to speed it up because its horribly slow.

See this answer on stack overflow: https://stackoverflow.com/a/28964449/7313127

To adapt this to the "CrumbPathRenderer" just add this obj C code to the drawMapRect function (This is just quick and dirty)

static dispatch_once_t onceToken;

dispatch_once(&onceToken, ^{

    CADisplayLink *displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(update)];

    [displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
});

Create an update method on the renderer that calls setNeedsDisplay

-(void) update {

[self setNeedsDisplay];

}

I also put renderer.setNeedsDisplay in (but its probably not needed)

func mapView(_ mapView: MKMapView, regionWillChangeAnimated animated:Bool)

{

 crumbRenderer.setNeedsDisplay()

}

Important NOTE: This will render flawlessly but will use 100% CPU. So to not drain the battery of the phone and thrash the CPU, in the update method keep a static and only call setNeedsDisplay every third time display link calls it. Remember CA display link is a hardware refresh timer.

If you follow this (hastily composed) answer which took me days to figure out, you will use about 30% CPU and your map paths with never show up ugly.

Hey apple, wanna fix MKMapView?