I need to figure out how to set up MKMapSnapshotterOptions to take a snapshot of aerial/satellite imagery associated with a polygonal region of the earth.
Filling out the 'region', 'scale', 'size', and 'mapType' properties are trivial, as I have an MKPolygon to work with. The tricky part is in setting the 'camera' -- in my particular case, I am using MKMapSnapshotter independently of the MKMapView (in fact, not even on the main thread).
However, I would prefer to orient the snapshot so that it fits the bounds of the polygon based on a non-zero heading -- that is, the area that I am taking a picture of has a 'start' and an 'end' that I would like to orient from the bottom to the top of the resulting image. Since the polygon is basically never going to be naturally oriented on a 0 degree heading, I will need to determine the 'centerCoordinate', 'heading', and 'altitude'.
As I have the polygon's coordinates, I am able to derive the center coordinate and desired heading fairly easily -- The first coordinate of the polygon correlates to the 'start' of the shape and the end (or other coordinate, in my case) correlates to the 'end'.
Figuring out the altitude is proving to be more difficult; I want to make sure that the polygon area ends up filling the aspect ratio of the snapshot image I wish to display. How do I calculate the correct altitude to use with the MKMapCamera without relying on MKMapView's 'setRegion' selector?
In order to solve this problem, I ended up doing the following:
1) rotating the MKPolygon around it's center coordinate to eliminate heading/rotation issues when determining a bounding rectangle: asking an MKPolygon for it's 'boundingMapRect' without this would return whatever minimum rectangle fit around the entirety of the shape. If a long, skinny polygon happened to be oriented diagonally from north-east to south-west, the bounding rect would be nearly square. Performing the rotation allows for the heading of the polygon to be taken into account in determining it's aspect ratio.
2) fitting the polygon's heading-corrected bounding rectangle into the aspect ratio of the snapshot viewport: this ensures that a very 'tall' polygon will still fit properly in a wide-aspect viewport and vice-versa.
3)[Removed from my example code] Creating a polygon of the resulting aspect-corrected bounding rectangle and rotating it back to the original heading using the polygon's center coordinate: This would likely be needed if working with large regions, as the next step involves measurement between horizontal/vertical bounding distances. In my case, I am working with very small regions that should not be impacted enough by the curvature of the earth to make a real difference.
4) determining the total horizontal and vertical bounding region in meters
5) using the larger dimension (Dimension) of the two distances to form the base measurement of a triangle, where A = minimum coordinate location on axis, B = maximum coordinate location on axis, and C = camera location (center coordinate of the polygon)
At this point, I was a bit stumped as to how to solve the altitude of the resulting triangle without having at least 1 of the angles. In performing some tests using an MKMapView instance, it appears that the aperture of the MKMapCamera is about 30 degrees -- this is regardless of augmenting the aspect ratio of the viewport, aspect ratio of the polygon, or any other factor than the curvature of the earth. I may be wrong about this assertion.
5) Using the aperture angle observed in my tests, calculate the needed altitude using (dimension / 2) / tan(aperture_angle_in_radians / 2)
Seeing how much time I ended up spending on this, I've decided to post the question/answer combo on StackOverflow in hopes that it either: 1) helps someone else in the same situation 2) is corrected by someone way smarter than I am and leads to an even better solution
Thanks!
OH, and of course, the code:
+ (double)determineAltitudeForPolygon:(MKPolygon *)polygon withHeading:(double)heading andWithViewport:(CGSize)viewport {
// Get a bounding rectangle that encompasses the polygon and represents its
// true aspect ratio based on the understanding of its heading.
MKMapRect boundingRect = [[self rotatePolygon:polygon withCenter:MKMapPointForCoordinate(polygon.coordinate) byHeading:heading] boundingMapRect];
MKCoordinateRegion boundingRectRegion = MKCoordinateRegionForMapRect(boundingRect);
// Calculate a new bounding rectangle that is corrected for the aspect ratio
// of the viewport/camera -- this will be needed to ensure the resulting
// altitude actually fits the polygon in view for the observer.
CLLocationCoordinate2D upperLeftCoord = CLLocationCoordinate2DMake(boundingRectRegion.center.latitude + boundingRectRegion.span.latitudeDelta / 2, boundingRectRegion.center.longitude - boundingRectRegion.span.longitudeDelta / 2);
CLLocationCoordinate2D upperRightCoord = CLLocationCoordinate2DMake(boundingRectRegion.center.latitude + boundingRectRegion.span.latitudeDelta / 2, boundingRectRegion.center.longitude + boundingRectRegion.span.longitudeDelta / 2);
CLLocationCoordinate2D lowerLeftCoord = CLLocationCoordinate2DMake(boundingRectRegion.center.latitude - boundingRectRegion.span.latitudeDelta / 2, boundingRectRegion.center.longitude - boundingRectRegion.span.longitudeDelta / 2);
CLLocationDistance hDist = MKMetersBetweenMapPoints(MKMapPointForCoordinate(upperLeftCoord), MKMapPointForCoordinate(upperRightCoord));
CLLocationDistance vDist = MKMetersBetweenMapPoints(MKMapPointForCoordinate(upperLeftCoord), MKMapPointForCoordinate(lowerLeftCoord));
double adjacent;
double newHDist, newVDist;
if (boundingRect.size.height > boundingRect.size.width) {
newVDist = vDist;
newHDist = (viewport.width / viewport.height) * vDist;
adjacent = vDist / 2;
} else {
newVDist = (viewport.height / viewport.width) * hDist;
newHDist = hDist;
adjacent = hDist / 2;
}
double result = adjacent / tan(Deg_to_Rad(15));
return result;
}
+ (MKPolygon *)rotatePolygon:(MKPolygon *)polygon withCenter:(MKMapPoint)centerPoint byHeading:(double)heading {
MKMapPoint points[polygon.pointCount];
double rotation_angle = -Deg_to_Rad(heading);
for(int i = 0; i < polygon.pointCount; i++) {
MKMapPoint point = polygon.points[i];
// Translate each point by the coordinate to rotate around, use matrix
// algebra to perform the rotation, then translate back into the
// original coordinate space.
double newX = ((point.x - centerPoint.x) * cos(rotation_angle)) + ((centerPoint.y - point.y) * sin(rotation_angle)) + centerPoint.x;
double newY = ((point.x - centerPoint.x) * sin(rotation_angle)) - ((centerPoint.y - point.y) * cos(rotation_angle)) + centerPoint.y;
point.x = newX;
point.y = newY;
points[i] = point;
}
return [MKPolygon polygonWithPoints:points count:polygon.pointCount];
}