Ákos Morvai
Ákos Morvai

Reputation: 211

Swift MapKit custom tiles and polylines break on zooming and moving

I am developing a navigation application that displays custom tiles and multicolour polylines on the map. When the map moves the polylines and the custom tiles break on the edge of the tiles. When the map is refreshed by the system the issue is solved, but this refresh is usually slow and the broken lines are visible on the screen for a couple of seconds.

enter image description here

Here you can see the issue. Normally the polyline's width is the thinner one, but sometimes the map draws the lines wider and then it stays on the screen until the next refresh.

(As you can see this is a CarPlay application but the issue tends the appear on a normal app.)

I use a custom class inherited from MKPolylineRenderer as this polyline has different colours based on some logic.

Like here:

enter image description here

Has anyone an idea what could case the issue and how to resolve it?

My code for the renderer class:

import MapKit
import Foundation

class CustomGradientPolylineRenderer: 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 }

        guard let polyLine = self.polyline as? GradientRoutePolyline else { return }
        context.setLineCap(.round)
        
        drawOneColorLine(on: context, lineWidth: (lineWidth - 1) / zoomScale, color: .white)
        drawGradientLine(on: context, lineWidth: (lineWidth - 6) / zoomScale, colors: polyLine.colors)
        
        super.draw(mapRect, zoomScale: zoomScale, in: context)
    }
    
    private func drawOneColorLine(on context: CGContext, lineWidth: Double, color: UIColor) {
        context.setLineWidth(lineWidth)
        let points = self.polyline.points()
        
        context.move(to: self.point(for: points[0]))
        for index in 1...self.polyline.pointCount - 1 {
            context.addLine(to: self.point(for: points[index]))
        }
        context.setStrokeColor(color.cgColor)
        context.strokePath()
    }
    
    private func drawGradientLine(on context: CGContext, lineWidth: Double, colors: [UIColor]) {
        context.setLineWidth(lineWidth)
        for index in 1...self.polyline.pointCount - 1 {
            let point = self.point(for: self.polyline.points()[index])
            let prevPoint = self.point(for: self.polyline.points()[index - 1])
            context.move(to: prevPoint)
            context.addLine(to: point)
            
            let currentColor = colors[index].cgColor
            context.setStrokeColor(currentColor)
            context.strokePath()
        }
    }
}

The GradientRoutePolyline class:

import MapKit
import Foundation

/// Inherits from MKPolyline and it represents the route which is drawn on the map as a polyline. It must be rendered with CustomGradientPolylineRenderer as it's color changes based on the car's charge level.
class GradientRoutePolyline: MKPolyline {
    var id: Int = 0
    var lineWidth: CGFloat = 0.0
    var colors: [UIColor] = []
    var locations: [CGFloat] = []
    
    /// Initializer for the class. Computes the different colors used on the route based on the RoutePoint array received as parameter. Then calls the default init with coordinates.
    /// - Parameters:
    ///   - routePoints: RoutePoints that hold the coordinates for the route and the inforamtion about the vehicle charge level for calculating colors.
    ///   - lineWidth: The line width to use.
    convenience init(routePoints: [RoutePoint], lineWidth: CGFloat) {
        let pointArray = routePoints.map {
            CLLocationCoordinate2D(latitude: $0.location.latitude, longitude: $0.location.longitude)
        }
        self.init(coordinates: pointArray, count: pointArray.count)
        self.lineWidth = lineWidth
        self.colors = routePoints.map {
            .init(red: 1 - $0.soC, green: $0.soC, blue: 0, alpha: 1)
        }
        self.locations = Array(0..<colors.count).map { CGFloat($0) / CGFloat(colors.count) }
    }
}

The code that sets and renders the custom tiles:

/// Creates a MapOverlay with a custom tile URL, and adds it to the mapView
    private func setupTileRenderer() {
        let skin = AccountService.shared.userSettings.mapSettings.skin.rawValue
        let poi = ""
        let template = "\(ConfigKey.TILE_URL.value)\(skin)\(poi)_en_v2/{z}/{x}/{y}.png?v=3"
        for overlay in mapView.overlays
        {
            if overlay is MapOverlay
            {
                mapView.removeOverlay(overlay)
            }
        }
        
        let tileOverlay = MapOverlay(urlTemplate: template)
        tileOverlay.canReplaceMapContent = true
        tileOverlay.maximumZ = Int(maxZoomLevel)
        
        mapView.insertOverlay(tileOverlay, at: 0)
    }
import MapKit
import Foundation

class MapOverlay: MKTileOverlay {
    override func url(forTilePath path: MKTileOverlayPath) -> URL {
        var tileUrl = self.urlTemplate!.replacingOccurrences(of: "{z}", with: "\(path.z)")
        tileUrl = tileUrl.replacingOccurrences(of: "{x}", with: "\(path.x)")
        tileUrl = tileUrl.replacingOccurrences(of: "{y}", with: "\(path.y)")
        return URL(string: tileUrl)!
    }
}

The way I move the map during navigation:

func setCamera(center: CLLocationCoordinate2D, headingAngle: Double, animated: Bool = true) {
        let camera = mapView.camera.copy() as! MKMapCamera
        camera.heading = headingAngle
        camera.pitch = CAMERA_PITCH
        camera.centerCoordinate = center
        camera.centerCoordinateDistance = currentCenterCoordinateDistance
        mapView.setCamera(camera, animated: animated)
    }

Upvotes: 0

Views: 253

Answers (1)

Gerd Castan
Gerd Castan

Reputation: 6849

Probably MKGradientPolylineRenderer already can do what you want.

If you still want to draw yourself: road width depends on the zoom level, see MKRoadWidthAtZoomScale https://developer.apple.com/documentation/mapkit/1452156-mkroadwidthatzoomscale?language=objc

and you might want do something like

override func draw(_ mapRect: MKMapRect, zoomScale: MKZoomScale, in context: CGContext) {
    let roadWidth = MKRoadWidthAtZoomScale(zoomScale)
    let scaledLineWidth = lineWidth * roadWidth
    context.setLineWidth(scaledLineWidth)
    ...
}

Upvotes: -1

Related Questions