11
votes

I'm trying to zoom to fit annotations on a map while locking the center and providing some insets.

- (void)fitAnnotations:(NSArray *)annotations edgeInsets:(UIEdgeInsets)insets
{
    CLLocationCoordinate2D originalCenter = self.centerCoordinate;
    MKMapRect mapRect = MKMapRectNull;

    for (id<MKAnnotation> annotation in annotations) {
        MKMapPoint p = MKMapPointForCoordinate([annotation coordinate]);
        mapRect = MKMapRectUnion(mapRect, MKMapRectMake(p.x, p.y, 0, 0));
    }

    mapRect = [self mapRectThatFits:mapRect edgePadding:insets];
    MKCoordinateRegion mapRegion = MKCoordinateRegionForMapRect(mapRect);

    // we now try to go back to the original center, while increasing the span by neccessary amount
    MKCoordinateSpan centeringDelta = MKCoordinateSpanMake(fabs(mapRegion.center.latitude - originalCenter.latitude), fabs(mapRegion.center.longitude - originalCenter.longitude));
    mapRegion.center = originalCenter;
    mapRegion.span.latitudeDelta += centeringDelta.latitudeDelta * 2.0;
    mapRegion.span.longitudeDelta += centeringDelta.longitudeDelta * 2.0;
    mapRegion = [self regionThatFits:mapRegion];
    [self setRegion:mapRegion animated:YES];
}

The first part of the code here works as expected: it zooms to fit while respecting the insets. However, it shifts the center.

I try to re-adjust the center after that, but it fails. I'm not sure if my math on the re-centering is correct.

4
Have you tried using MKCoordinateRegionMake to create the new map region? That will allow you to pass in the centerCoordinate and a span - rmp
@rmp Right but then when you shift the center, some of the annotations you made sure fit are now out of bounds. - Snowman
you should be able to pass in the original center and not shift it - rmp
If there is an annotation on (0,0) and I shift the center by (0, -5), then that top annotation will now be at (0, -5). This is what I'm trying to avoid. - Snowman
if you pass your newly calculated span with the centerCoordinate the MKCoordinateRegionMake should take care of this - rmp

4 Answers

1
votes

The first part of your code that calculates the bounding map rect that fits the annotations is OK.

Only the adjustment of that "minimal" map rect so that the "locked" center is actually in the center needs to be corrected.

The main problem, I believe, is that the code in the question is adjusting the span to both re-center the region and account for the insets after calling mapRectThatFits: (which will itself already give a slightly modified version of the rect you actually requested).

Instead, your code should only calculate the actual, minimal rect it wants and then finally call setVisibleMapRect:edgePadding:animated: and let the map view figure out both the "rect that fits" and the insets.

Please try the following:

- (void)fitAnnotations:(NSArray *)annotations edgeInsets:(UIEdgeInsets)insets
{
    MKMapPoint centerMapPoint = MKMapPointForCoordinate(originalCenter);

    //--- First create minimal bounding map rect to tightly fit annotations...
    MKMapRect minimalMapRect = MKMapRectNull;

    for (id<MKAnnotation> annotation in annotations) {
        MKMapPoint annMapPoint = MKMapPointForCoordinate(annotation.coordinate);
        minimalMapRect = MKMapRectUnion(minimalMapRect, MKMapRectMake(annMapPoint.x, annMapPoint.y, 0, 0));
    }


    //--- Now create adjusted map rect so center coordinate is in the center...

    //distance of desired center from minimal left edge...
    double centerXOffset = centerMapPoint.x - minimalMapRect.origin.x;

    //raw amount width needs to be adjusted to get center in center...
    //negative/positive indicates whether center is in left/right half
    double widthOffset = 2.0 * centerXOffset - minimalMapRect.size.width;

    //add absolute value of raw width offset to minimal width...
    double adjustedWidth = minimalMapRect.size.width + fabs(widthOffset);

    //distance of desired center from minimal top edge...
    double centerYOffset = centerMapPoint.y - minimalMapRect.origin.y;

    //raw amount height needs to be adjusted to get center in center...
    //negative/positive indicates whether center is in top/bottom half
    double heightOffset = 2.0 * centerYOffset - minimalMapRect.size.height;

    //add absolute value of raw height offset to minimal height...
    double adjustedHeight = minimalMapRect.size.height + fabs(heightOffset);

    //adjust origin if necessary (if center is in top/left half)...
    MKMapPoint adjustedOrigin = minimalMapRect.origin;
    if ((centerXOffset / minimalMapRect.size.width) < 0.5)
    {
        adjustedOrigin.x = adjustedOrigin.x + widthOffset;
    }
    if ((centerYOffset / minimalMapRect.size.height) < 0.5)
    {
        adjustedOrigin.y = adjustedOrigin.y + heightOffset;
    }

    //create adjusted MKMapRect...
    MKMapRect adjustedMapRect = MKMapRectMake(adjustedOrigin.x, adjustedOrigin.y, adjustedWidth, adjustedHeight);


    //--- Apply the adjusted map rect with insets to map view...
    [mapView setVisibleMapRect:adjustedMapRect edgePadding:insets animated:YES];
}
0
votes

Try something like this, where you use your calculated mapRect to create the new region with your originalCenter via the MKCoordinateRegionMake method

MKCoordinateRegion mapRegion = MKCoordinateRegionForMapRect(mapRect);
mapRegion = MKCoordinateRegionMake(originalCenter, mapRegion.span);
mapView.region = mapRegion;
0
votes

Try this.

MKMapPoint center = MKMapPointForCoordinate(self.mapView.centerCoordinate);

double maxX = 0;
double maxY = 0;
for (MKPointAnnotation *a in self.mapView.annotations)
{
    MKMapPoint p = MKMapPointForCoordinate(a.coordinate);
    double deltaX = fabs(center.x - p.x);
    double deltaY = fabs(center.y - p.y);
    maxX = MAX(maxX, deltaX);
    maxY = MAX(maxY, deltaY);
}


MKMapRect rect = MKMapRectMake(center.x - maxX, center.y - maxY, maxX * 2, maxY * 2);
rect = [self.mapView mapRectThatFits:rect edgePadding:UIEdgeInsetsMake(20, 20, 20, 20)];

[self.mapView setVisibleMapRect:rect animated:1];
0
votes

@moby I am thinking of a different approach. How about taking the maps centre location as you already did. Now calculate distance to each annotation from this centre coordinate till you find the longest annotation (say 'requiredDistance' ). Get a new map rect with all your annotations plotted with same centre using below code:

    MKCircle *circleLine = [MKCircle circleWithCenterCoordinate:self.centerCoordinate radius:requiredDistance];
    [self.mapView setVisibleMapRect:circleLine.boundingMapRect];

Coming to your insets what ever insets you wanted to apply should be applied to your 'requiredDistace' variable in such a way that your 'requiredDistance' variable has a value always greater than or equal to the distance between your centre coordinate and your longest annotation to make sure all the annotations are always visible.