How to Determine the Correct Altitude for an Mkmapcamera Focusing on an Mkpolygon

How to determine the correct altitude for an MKMapCamera focusing on an MKPolygon

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];
}

How is the altitude of MKMapCamera calculated?

I found your post when asking the same question. I then found this post:

How to determine the correct altitude for an MKMapCamera focusing on an MKPolygon

Condensing this into the answer to your question (and mine):

    double distance = MKMetersBetweenMapPoints(MKMapPointForCoordinate(pinCenter.coordinate),
MKMapPointForCoordinate(pinEye.coordinate));
double altitude = distance / tan(M_PI*(15/180.0));

MKMapCamera *camera = [MKMapCamera cameraLookingAtCenterCoordinate:pinCenter.coordinate
fromEyeCoordinate:pinEye.coordinate
eyeAltitude:altitude];

MKMapCamera doesn't zoom to correct altitude

I don't know for certain why this is happening but I have a theory. When you're using the flyover map types the minimum altitude of the camera is restricted by the tallest structure in the centre of the map.

If you go to the Maps app, set it to 3D Satellite view and go directly above a tall building (say the Empire State Building in New York) you can only pinch to zoom to a little above the height of the building. If you pan the camera away from the tall structure you can pinch to zoom in further. The map won't let you zoom through or inside the structure. If you zoom in to the entrance of a tall building and try to pan towards the building, the map will adjust the altitude upwards without you pinching to zoom out to prevent you passing through the building.

So before the map is fully loaded, it doesn't know what the tallest structure at the centre is going to be. To prevent you zooming inside a tall structure, the map limits the minimum height. After the map is fully loaded and it knows that there are no tall structures it lets you zoom in closer.

When you set a long duration on the animation, it's giving the map a chance to load before it gets to the lower altitude. The map knows that there are no tall structures and allows further zooming in. I would say that if you tried a longer duration animation but throttled the network bandwidth it would stop working again.

Note that the Satellite mode allows you to pass through tall structures.

As a workaround, try using mapViewDidFinishLoadingMap: or mapViewDidFinishRenderingMap:fullyRendered: to know when to zoom in more.

Swift Mapkit max min zoom

You could use the mapView:regionDidChangeAnimated: delegate method to listen for region change events, and if the region is wider/narrower than your maximum/minimum region, set it back to the max/min region with setRegion:animated: to indicate to your user that they can't zoom out/in that far.

e.g.

func mapView(_ mapView: MKMapView, regionDidChangeAnimated animated: Bool) {
let coordinate = CLLocationCoordinate2DMake(mapView.region.center.latitude, mapView.region.center.longitude)
var span = mapView.region.span
if span.latitudeDelta < 0.002 { // MIN LEVEL
span = MKCoordinateSpanMake(0.002, 0.002)
} else if span.latitudeDelta > 0.003 { // MAX LEVEL
span = MKCoordinateSpanMake(0.003, 0.003)
}
let region = MKCoordinateRegionMake(coordinate, span)
mapView.setRegion(region, animated:true)
}

iOS 6 MkMapView preserve rotation when changing region

I had this exact same problem and ended up abandoning the [mapView setRegion:] method entirely, in favor of [mapView setCamera:] using the original region and heading as the basis for how to orient the camera.

MKCoordinateRegion currentRegion = MKCoordinateRegionMake(center, span);

double altitude = [self determineAltitudeForMapRect:MKMapRectForCoordinateRegion(currentRegion) withHeading:_heading andWithViewport:[[UIScreen mainScreen] bounds].size];

MKMapCamera *currentCamera = [MKMapCamera new];
[currentCamera setHeading:_heading];
[currentCamera setCenterCoordinate:center];
[currentCamera setAltitude:altitude];

[_mapView setCamera:currentCamera];

The trick with this option was how to determine [currentCamera setAltitude:] value, which would normally have been set automatically with [mapView setRegion:]

My solution was an adaptation this answer https://stackoverflow.com/a/21034410/1130983 where it uses some simple trig to determine the altitude, assuming the map camara has about a 30 degree viewing angle. However, instead of passing in a polygon, I'm passing in the MKMapRect directly:

- (double)determineAltitudeForMapRect:(MKMapRect)boundingRect 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.
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;

if (boundingRect.size.height > boundingRect.size.width)
{
adjacent = vDist / 2;
}
else
{
adjacent = hDist / 2;
}

double result = adjacent / tan(DEGREES_TO_RADIANS(15));
return result;
}

Bounding box coordinates for MKMapSnapshotter result

Turns out it just requires some simple maths:

    func printTranslatedCoordinates(coordinate: CLLocationCoordinate2D, latitudeMetres: Double, longitudeMetres: Double) {
let region = MKCoordinateRegionMakeWithDistance(coordinate, latitudeMetres, longitudeMetres)
let span = region.span

let northLatitude = coordinate.latitude + span.latitudeDelta
let eastLongitude = coordinate.longitude + span.longitudeDelta
let southLatitude = coordinate.latitude - span.latitudeDelta
let westLongitude = coordinate.longitude - span.longitudeDelta
print("\(westLongitude) \(southLatitude) \(eastLongitude) \(northLatitude)")
}


Related Topics



Leave a reply



Submit