I have an app where users can add an image to a map. This is pretty straightforward. It becomes much more difficult when I want to add rotation (taken from the current map heading). The code I use to create an image is pretty straightforward:
let imageAspectRatio = Double(image.size.height / image.size.width)
let mapAspectRatio = Double(visibleMapRect.size.height / visibleMapRect.size.width)
var mapRect = visibleMapRect
if mapAspectRatio > imageAspectRatio {
// Aspect ratio of map is bigger than aspect ratio of image (map is higher than the image), take away height from the rectangle
let heightChange = mapRect.size.height - mapRect.size.width * imageAspectRatio
mapRect.size.height = mapRect.size.width * imageAspectRatio
mapRect.origin.y += heightChange / 2
} else {
// Aspect ratio of map is smaller than aspect ratio of image (map is higher than the image), take away width from the rectangle
let widthChange = mapRect.size.width - mapRect.size.height / imageAspectRatio
mapRect.size.width = mapRect.size.height / imageAspectRatio
mapRect.origin.x += widthChange / 2
}
photos.append(ImageOverlay(image: image, boundingMapRect: mapRect, rotation: cameraHeading))
The ImageOverlay class inherits from MKOverlay, which I can easily draw on the map. Here's the code for that class:
class ImageOverlay: NSObject, MKOverlay {
let image: UIImage
let boundingMapRect: MKMapRect
let coordinate: CLLocationCoordinate2D
let rotation: CLLocationDirection
init(image: UIImage, boundingMapRect: MKMapRect, rotation: CLLocationDirection) {
self.image = UIImage.fixedOrientation(for: image) ?? image
self.boundingMapRect = boundingMapRect
self.coordinate = CLLocationCoordinate2D(latitude: boundingMapRect.midX, longitude: boundingMapRect.midY)
self.rotation = rotation
}
}
I have figured out by how much I need to scale and translate the context to fit on the correct location on the map (from which it was added). I can't figure out how to rotate the context to make the image render in the correct location.
I figured out that the map rotation was in degrees, and the rotate method takes radians (took longer than I dare to admit), but the image moves around when I apply the rotation.
I use the following code to render the overlay:
override func draw(_ mapRect: MKMapRect, zoomScale: MKZoomScale, in context: CGContext) {
guard let overlay = overlay as? ImageOverlay else {
return
}
let rect = self.rect(for: overlay.boundingMapRect)
context.scaleBy(x: 1.0, y: -1.0)
context.translateBy(x: 0.0, y: -rect.size.height)
context.rotate(by: CGFloat(overlay.rotation * Double.pi / 180))
context.draw(overlay.image.cgImage!, in: rect)
}
How do I need to rotate this context to get the image to be aligned properly?
This is an open source project with the code here
Edit: I have tried (and failed) to use some kind of trig function. If I scale by a factor of 3 * sin(rotation) / 4 (no clue where the 3/4 comes from), I get a correct scale for some rotations, but not for others.
It sounds like you trying to rotate the object in it's local coordinates, but are actually rotating in the world coordinates. I admit I'm not familiar with this library, but the moral of the story is that order of operation on transformations matter. But it looks like you have "TranslateBy" and you are sending in zero, which might not be moving it at all? If you are trying to translate back to local you'd need to translate to local by subtracting it's current coordinates in the CLLocationCoordinate2D struct.
This should allow the image to be in the correct position but now rotated to align with the heading.
Here is a paper which explains what you are probably encountering, although more in depth and specific to the matrix/opengl, but the first slide illustrates your issue.
Transforms PDF