Search code examples
iosmapkitcore-graphics

Render a MKPolyline in a UIImage


I need to render a MKPolyline in a UIImage without the use of a MKMapView if possible (for performances reasons mainly, I want to display the path as soon as possible, while the Map screenshotter provides asynchronously the map, like the app Strava does).

I came up with this piece of code:

func pathImageForSize(size: CGSize) -> UIImage {
    let region = regionForPath() // MKCoordinateRegion
    let coordinates = locations.map { $0.coordinate }
    UIGraphicsBeginImageContextWithOptions(size, false, UIScreen.mainScreen().scale)

    let context = UIGraphicsGetCurrentContext()

    CGContextSetStrokeColorWithColor(context, UIColor.palette_mainColor().CGColor)
    CGContextSetLineWidth(context, 4.0)
    CGContextBeginPath(context)

    let origin = CGPoint(x: region.center.latitude - region.span.latitudeDelta / 2, y: region.center.longitude - region.span.longitudeDelta / 2)

    for (index, coordinate) in coordinates.enumerate() {
        let point = CGPoint(x: ((CGFloat(coordinate.latitude) - origin.x) / CGFloat(region.span.latitudeDelta)) * size.width,
                            y: ((CGFloat(coordinate.longitude) - origin.y) / CGFloat(region.span.longitudeDelta)) * size.height)

        if index == 0 {
            CGContextMoveToPoint(context, point.x, point.y)
        } else {
            CGContextAddLineToPoint(context, point.x, point.y)
        }
    }

    CGContextStrokePath(context)

    let image = UIGraphicsGetImageFromCurrentImageContext()
    UIGraphicsEndImageContext()
    return image
}

The code basically rescales the coordinates and draws them with CoreGraphics. The path is drawn, but it's also rotated by 90 degrees, as you can see here:

Polyline rendered by MapKit: MapKit polyline

Polyline rendered by my code: CoreGraphics polyline

Any idea on what I'm doing wrong?


Solution

  • Ok, figured it out, my premise was wrong to begin with: The region that I was calculating encapsulated the path's coordinates, but this region is adjusted by the mapview when it's time to render. The solution was to scale the region to fit the mapview's frame:

    let map = MKMapView(frame: CGRect(origin: CGPoint.zero, size: size))
    let region = map.regionThatFits(regionForPath())
    

    Also, as suggested by BrentM, the coordinates were flipped. The resulting code is this:

    func pathImageForSize(size: CGSize) -> UIImage {
      let map = MKMapView(frame: CGRect(origin: CGPoint.zero, size: size))
      let region = map.regionThatFits(regionForPath())
      let coordinates = locations.map { $0.coordinate }
      UIGraphicsBeginImageContextWithOptions(size, false, UIScreen.mainScreen().scale)
    
      let context = UIGraphicsGetCurrentContext()
      CGContextSetLineJoin(context, .Round)
      CGContextSetLineCap(context, .Round)
      CGContextSetStrokeColorWithColor(context, UIColor.palette_mainColor().CGColor)
      CGContextSetLineWidth(context, 4.0)
      CGContextBeginPath(context)
    
      let origin = CGPoint(x: region.center.longitude - region.span.longitudeDelta / 2, y: region.center.latitude - region.span.latitudeDelta / 2)
    
      for (index, coordinate) in coordinates.enumerate() {
        let point = CGPoint(x: ((CGFloat(coordinate.longitude) - origin.x) / CGFloat(region.span.longitudeDelta)) * size.width,
                            y: ((CGFloat(coordinate.latitude) - origin.y) / CGFloat(region.span.latitudeDelta)) * size.height)
    
        if index == 0 {
          CGContextMoveToPoint(context, point.x, size.height - point.y)
        } else {
          CGContextAddLineToPoint(context, point.x, size.height - point.y)
        }
      }
    
      CGContextStrokePath(context)
    
      let image = UIGraphicsGetImageFromCurrentImageContext()
      UIGraphicsEndImageContext()
      return image
    }