Positioning MKMapView to show multiple annotations at once
IosObjective CIphoneCocoa TouchMapkitIos Problem Overview
I've got several annotations I want to add to my MKMapView (it could 0-n items, where n is generally around 5). I can add the annotations fine, but I want to resize the map to fit all annotations onscreen at once, and I'm not sure how to do this.
I've been looking at -regionThatFits:
but I'm not quite sure what to do with it. I'll post some code to show what I've got so far. I think this should be a generally straightforward task but I'm feeling a bit overwhelmed with MapKit so far.
- (void)locationManager:(CLLocationManager *)manager didUpdateToLocation:(CLLocation *)newLocation fromLocation:(CLLocation *)oldLocation{
location = newLocation.coordinate;
//One location is obtained.. just zoom to that location
MKCoordinateRegion region;
region.center = location;
//Set Zoom level using Span
MKCoordinateSpan span;
span.latitudeDelta = 0.015;
span.longitudeDelta = 0.015;
region.span = span;
// Set the region here... but I want this to be a dynamic size
// Obviously this should be set after I've added my annotations
[mapView setRegion:region animated:YES];
// Test data, using these as annotations for now
NSArray *arr = [NSArray arrayWithObjects:@"one", @"two", @"three", @"four", nil];
float ex = 0.01;
for (NSString *s in arr) {
JBAnnotation *placemark = [[JBAnnotation alloc] initWithLat:(location.latitude + ex) lon:location.longitude];
[mapView addAnnotation:placemark];
ex = ex + 0.005;
}
// What do I do here?
[mapView setRegion:[mapView regionThatFits:region] animated:YES];
}
Notice, this all happens as I receive a location update... I don't know if that's an appropriate place to do this. If not, where would be a better place? -viewDidLoad
?
Thanks in advance.
Ios Solutions
Solution 1 - Ios
The link posted by Jim is now dead, but i was able to find the code (which I had bookmarked somewhere). Hope this helps.
- (void)zoomToFitMapAnnotations:(MKMapView *)mapView {
if ([mapView.annotations count] == 0) return;
CLLocationCoordinate2D topLeftCoord;
topLeftCoord.latitude = -90;
topLeftCoord.longitude = 180;
CLLocationCoordinate2D bottomRightCoord;
bottomRightCoord.latitude = 90;
bottomRightCoord.longitude = -180;
for(id<MKAnnotation> annotation in mapView.annotations) {
topLeftCoord.longitude = fmin(topLeftCoord.longitude, annotation.coordinate.longitude);
topLeftCoord.latitude = fmax(topLeftCoord.latitude, annotation.coordinate.latitude);
bottomRightCoord.longitude = fmax(bottomRightCoord.longitude, annotation.coordinate.longitude);
bottomRightCoord.latitude = fmin(bottomRightCoord.latitude, annotation.coordinate.latitude);
}
MKCoordinateRegion region;
region.center.latitude = topLeftCoord.latitude - (topLeftCoord.latitude - bottomRightCoord.latitude) * 0.5;
region.center.longitude = topLeftCoord.longitude + (bottomRightCoord.longitude - topLeftCoord.longitude) * 0.5;
// Add a little extra space on the sides
region.span.latitudeDelta = fabs(topLeftCoord.latitude - bottomRightCoord.latitude) * 1.1;
region.span.longitudeDelta = fabs(bottomRightCoord.longitude - topLeftCoord.longitude) * 1.1;
region = [mapView regionThatFits:region];
[mapView setRegion:region animated:YES];
}
Solution 2 - Ios
Why so complicated?
MKCoordinateRegion coordinateRegionForCoordinates(CLLocationCoordinate2D *coords, NSUInteger coordCount) {
MKMapRect r = MKMapRectNull;
for (NSUInteger i=0; i < coordCount; ++i) {
MKMapPoint p = MKMapPointForCoordinate(coords[i]);
r = MKMapRectUnion(r, MKMapRectMake(p.x, p.y, 0, 0));
}
return MKCoordinateRegionForMapRect(r);
}
Solution 3 - Ios
As of iOS7 you can use showAnnotations:animated:
[mapView showAnnotations:annotations animated:YES];
Solution 4 - Ios
I have done something similiar to this to zoom out (or in) to an area that included a point annotation and the current location. You could expand this by looping through your annotations.
The basic steps are:
- Calculate the min lat/long
- Calculate the max lat/long
- Create CLLocation objects for these two points
- Calculate distance between points
- Create region using center point
between points and distance converted
to degrees
- Pass region into MapView to adjust
- Use adjusted region to set MapView region
-(IBAction)zoomOut:(id)sender {
CLLocationCoordinate2D southWest = _newLocation.coordinate;
CLLocationCoordinate2D northEast = southWest;
southWest.latitude = MIN(southWest.latitude, _annotation.coordinate.latitude);
southWest.longitude = MIN(southWest.longitude, _annotation.coordinate.longitude);
northEast.latitude = MAX(northEast.latitude, _annotation.coordinate.latitude);
northEast.longitude = MAX(northEast.longitude, _annotation.coordinate.longitude);
CLLocation *locSouthWest = [[CLLocation alloc] initWithLatitude:southWest.latitude longitude:southWest.longitude];
CLLocation *locNorthEast = [[CLLocation alloc] initWithLatitude:northEast.latitude longitude:northEast.longitude];
// This is a diag distance (if you wanted tighter you could do NE-NW or NE-SE)
CLLocationDistance meters = [locSouthWest getDistanceFrom:locNorthEast];
MKCoordinateRegion region;
region.center.latitude = (southWest.latitude + northEast.latitude) / 2.0;
region.center.longitude = (southWest.longitude + northEast.longitude) / 2.0;
region.span.latitudeDelta = meters / 111319.5;
region.span.longitudeDelta = 0.0;
_savedRegion = [_mapView regionThatFits:region];
[_mapView setRegion:_savedRegion animated:YES];
[locSouthWest release];
[locNorthEast release];
}
Solution 5 - Ios
I have a different answer. I was going to do implement the zoom-to-fit algorithm myself, but i figured that Apple must have a way to do what we wanted without so much work. Using the API doco quickly showed that I could use MKPolygon to do what was needed:
/* this simply adds a single pin and zooms in on it nicely */
- (void) zoomToAnnotation:(MapAnnotation*)annotation {
MKCoordinateSpan span = {0.027, 0.027};
MKCoordinateRegion region = {[annotation coordinate], span};
[mapView setRegion:region animated:YES];
}
/* This returns a rectangle bounding all of the pins within the supplied
array */
- (MKMapRect) getMapRectUsingAnnotations:(NSArray*)theAnnotations {
MKMapPoint points[[theAnnotations count]];
for (int i = 0; i < [theAnnotations count]; i++) {
MapAnnotation *annotation = [theAnnotations objectAtIndex:i];
points[i] = MKMapPointForCoordinate(annotation.coordinate);
}
MKPolygon *poly = [MKPolygon polygonWithPoints:points count:[theAnnotations count]];
return [poly boundingMapRect];
}
/* this adds the provided annotation to the mapview object, zooming
as appropriate */
- (void) addMapAnnotationToMapView:(MapAnnotation*)annotation {
if ([annotations count] == 1) {
// If there is only one annotation then zoom into it.
[self zoomToAnnotation:annotation];
} else {
// If there are several, then the default behaviour is to show all of them
//
MKCoordinateRegion region = MKCoordinateRegionForMapRect([self getMapRectUsingAnnotations:annotations]);
if (region.span.latitudeDelta < 0.027) {
region.span.latitudeDelta = 0.027;
}
if (region.span.longitudeDelta < 0.027) {
region.span.longitudeDelta = 0.027;
}
[mapView setRegion:region];
}
[mapView addAnnotation:annotation];
[mapView selectAnnotation:annotation animated:YES];
}
Hope this helps.
Solution 6 - Ios
you can also do it this way..
// Position the map so that all overlays and annotations are visible on screen.
MKMapRect regionToDisplay = [self mapRectForAnnotations:annotationsToDisplay];
if (!MKMapRectIsNull(regionToDisplay)) myMapView.visibleMapRect = regionToDisplay;
- (MKMapRect) mapRectForAnnotations:(NSArray*)annotationsArray
{
MKMapRect mapRect = MKMapRectNull;
//annotations is an array with all the annotations I want to display on the map
for (id<MKAnnotation> annotation in annotations) {
MKMapPoint annotationPoint = MKMapPointForCoordinate(annotation.coordinate);
MKMapRect pointRect = MKMapRectMake(annotationPoint.x, annotationPoint.y, 0, 0);
if (MKMapRectIsNull(mapRect))
{
mapRect = pointRect;
} else
{
mapRect = MKMapRectUnion(mapRect, pointRect);
}
}
return mapRect;
}
Solution 7 - Ios
Based on the information and suggestions from everyone I came up with the following. Thanks for everyone in this discussion for contributing :) This would go in the view Controller that contains the mapView.
- (void)zoomToFitMapAnnotations {
if ([self.mapView.annotations count] == 0) return;
int i = 0;
MKMapPoint points[[self.mapView.annotations count]];
//build array of annotation points
for (id<MKAnnotation> annotation in [self.mapView annotations])
points[i++] = MKMapPointForCoordinate(annotation.coordinate);
MKPolygon *poly = [MKPolygon polygonWithPoints:points count:i];
[self.mapView setRegion:MKCoordinateRegionForMapRect([poly boundingMapRect]) animated:YES];
}
Solution 8 - Ios
Using Swift, a polygon, and some extra padding I used the following:
func zoomToFit() {
var allLocations:[CLLocationCoordinate2D] = [
CLLocationCoordinate2D(latitude: 32.768805, longitude: -117.167119),
CLLocationCoordinate2D(latitude: 32.770480, longitude: -117.148385),
CLLocationCoordinate2D(latitude: 32.869675, longitude: -117.212929)
]
var poly:MKPolygon = MKPolygon(coordinates: &allLocations, count: allLocations.count)
self.mapView.setVisibleMapRect(poly.boundingMapRect, edgePadding: UIEdgeInsetsMake(40.0, 40.0, 40.0, 40.0), animated: false)
}
Solution 9 - Ios
In my case, I am starting with CLLocation objects and creating annotations for each of them.
I only need to place two annotations, so I have a simple approach to building the array of points, but it could easily be expanded to build an array with an arbitrary length given a set of CLLocations.
Here's my implementation (doesn't require creating MKMapPoints):
//start with a couple of locations
CLLocation *storeLocation = store.address.location.clLocation;
CLLocation *userLocation = [LBLocationController sharedController].currentLocation;
//build an array of points however you want
CLLocationCoordinate2D points[2] = {storeLocation.coordinate, userLocation.coordinate};
//the magic part
MKPolygon *poly = [MKPolygon polygonWithCoordinates:points count:2];
[self.mapView setRegion:MKCoordinateRegionForMapRect([poly boundingMapRect])];
Solution 10 - Ios
Here is the SWIFT Equivalent (Confirmed Working in : Xcode6.1, SDK 8.2) for Mustafa's Answers:
func zoomToFitMapAnnotations() {
if self.annotations.count == 0 {return}
var topLeftCoordinate = CLLocationCoordinate2D(latitude: -90, longitude: 180)
var bottomRightCoordinate = CLLocationCoordinate2D(latitude: 90, longitude: -180)
for object in self.annotations {
if let annotation = object as? MKAnnotation {
topLeftCoordinate.longitude = fmin(topLeftCoordinate.longitude, annotation.coordinate.longitude)
topLeftCoordinate.latitude = fmax(topLeftCoordinate.latitude, annotation.coordinate.latitude)
bottomRightCoordinate.longitude = fmax(bottomRightCoordinate.longitude, annotation.coordinate.longitude)
bottomRightCoordinate.latitude = fmin(bottomRightCoordinate.latitude, annotation.coordinate.latitude)
}
}
let center = CLLocationCoordinate2D(latitude: topLeftCoordinate.latitude - (topLeftCoordinate.latitude - bottomRightCoordinate.latitude) * 0.5, longitude: topLeftCoordinate.longitude - (topLeftCoordinate.longitude - bottomRightCoordinate.longitude) * 0.5)
print("\ncenter:\(center.latitude) \(center.longitude)")
// Add a little extra space on the sides
let span = MKCoordinateSpanMake(fabs(topLeftCoordinate.latitude - bottomRightCoordinate.latitude) * 1.01, fabs(bottomRightCoordinate.longitude - topLeftCoordinate.longitude) * 1.01)
print("\nspan:\(span.latitudeDelta) \(span.longitudeDelta)")
var region = MKCoordinateRegion(center: center, span: span)
region = self.regionThatFits(region)
self.setRegion(region, animated: true)
}
Solution 11 - Ios
I know this is an old question but, if you want to display all the annotations ALREADY ON the map use this:
mapView.showAnnotations(mapView.annotations, animated: true)
Solution 12 - Ios
There's a new method in 'MKMapView' as of iOS 7 that you can use
> Declaration > > SWIFT > > func showAnnotations(_ annotations: [AnyObject]!, > animated animated: Bool) > > OBJECTIVE-C > > - (void)showAnnotations:(NSArray *)annotations > animated:(BOOL)animated > > Parameters > > annotations The annotations that you want to be visible in > the map. animated YES if you want the map region change to be > animated, or NO if you want the map to display the new region > immediately without animations. > > Discussion > > Calling this method updates > the value in the region property and potentially other properties to > reflect the new map region.
Solution 13 - Ios
Based on the excellent answer by me2
(now in Swift)
private func coordinateRegion(for coordinates: [CLLocationCoordinate2D]) -> MKCoordinateRegion {
var rect: MKMapRect = .null
for coordinate in coordinates {
let point: MKMapPoint = MKMapPoint(coordinate)
rect = rect.union(MKMapRect(x: point.x, y: point.y, width: 0, height: 0))
}
return MKCoordinateRegion(rect)
}
Solution 14 - Ios
One possible solution might be measuring the distance between the current location and all the annotations and using the MKCoordinateRegionMakeWithDistance method to make a region that has a slightly greater distance than the furthest annotation.
This would of course get slower the more annotations you added though.
Solution 15 - Ios
- (void)zoomToFitMapAnnotations {
if ([self.mapview.annotations count] == 0) return;
int i = 0;
MKMapPoint points[[self.mapview.annotations count]];
//build array of annotation points
for (id<MKAnnotation> annotation in [self.mapview annotations])
points[i++] = MKMapPointForCoordinate(annotation.coordinate);
MKPolygon *poly = [MKPolygon polygonWithPoints:points count:i];
[self.mapview setRegion:MKCoordinateRegionForMapRect([poly boundingMapRect]) animated:YES];
}
Solution 16 - Ios
Adding additional to Stéphane de Luca's answer. Here we can keep UserLocation plus Custom annotations to fit MKMapView
private func centerViewOnUserLocation() {
if selectedLatitudeFromPreviousVC?.description != nil && selectedLongitudeFromPreviousVC?.description != nil {
if let location = locationManager.location?.coordinate {
let region = regionFor(coordinates: [
CLLocationCoordinate2D(latitude: selectedLatitudeFromPreviousVC!, longitude: selectedLongitudeFromPreviousVC!),
location])
mkmapView.setRegion(region, animated: true)
}
} else {
if let location = locationManager.location?.coordinate {
let region = MKCoordinateRegion.init(center: location, latitudinalMeters: regionInMeters, longitudinalMeters: regionInMeters)
mkmapView.setRegion(region, animated: true)
}
}
}
private func regionFor(coordinates coords: [CLLocationCoordinate2D]) -> MKCoordinateRegion {
var r = MKMapRect.null
for i in 0 ..< coords.count {
let p = MKMapPoint(coords[i])
r = r.union(MKMapRect(x: p.x, y: p.y, width: 0, height: 0))
}
var coordinateRegion = MKCoordinateRegion(r)
coordinateRegion.span.latitudeDelta *= 1.5
coordinateRegion.span.longitudeDelta *= 1.5
return coordinateRegion
}
Solution 17 - Ios
Added a little if clause to handle 1 location- to add to mustufa's cound code snippet. Used pkclSoft's zoomToAnnotation function for that:
if ([mapView.annotations count] == 1){
MKCoordinateSpan span = {0.027, 0.027};
region.span = span;
CLLocationCoordinate2D singleCoordinate = [[mapView.annotations objectAtIndex:0] coordinate];
region.center.latitude = singleCoordinate.latitude;
region.center.longitude = singleCoordinate.longitude;
}
else
{
// mustufa's code
}
Solution 18 - Ios
this code works for me, it shows all the pins with current location, hope this helps you,
func setCenterForMap() {
var mapRect: MKMapRect = MKMapRectNull
for loc in mapView.annotations {
let point: MKMapPoint = MKMapPointForCoordinate(loc.coordinate)
print( "location is : \(loc.coordinate)");
mapRect = MKMapRectUnion(mapRect, MKMapRectMake(point.x,point.y,0,0))
}
if (locationManager.location != nil) {
let point: MKMapPoint = MKMapPointForCoordinate(locationManager.location!.coordinate)
print( "Cur location is : \(locationManager.location!.coordinate)");
mapRect = MKMapRectUnion(mapRect, MKMapRectMake(point.x,point.y,0,0))
}
mapView.setVisibleMapRect(mapRect, edgePadding: UIEdgeInsetsMake(40.0, 40.0, 40.0, 40.0), animated: true)
}
Solution 19 - Ios
I hope this is at least relevant, this is what I put together for Mono (based off of pkclSoft's answer):
void ZoomMap (MKMapView map)
{
var annotations = map.Annotations;
if (annotations == null || annotations.Length == 0)
return;
var points = annotations.OfType<MapAnnotation> ()
.Select (s => MKMapPoint.FromCoordinate (s.Coordinate))
.ToArray ();
map.SetVisibleMapRect(MKPolygon.FromPoints (points).BoundingMapRect, true);
}
Solution 20 - Ios
CLLocationCoordinate2D min = CLLocationCoordinate2DMake(99999.0, 99999.0);
CLLocationCoordinate2D max = CLLocationCoordinate2DMake(-99999.0, -99999.0);
// find max/min....
// zoom to cover area
// TODO: Maybe better using a MKPolygon which can calculate its own fitting region.
CLLocationCoordinate2D center = CLLocationCoordinate2DMake((max.latitude + min.latitude) / 2.0, (max.longitude + min.longitude) / 2.0);
MKCoordinateSpan span = MKCoordinateSpanMake(max.latitude - min.latitude, max.longitude - min.longitude);
MKCoordinateRegion region = MKCoordinateRegionMake(center, span);
[_mapView setRegion:[_mapView regionThatFits:region] animated:YES];
Solution 21 - Ios
Based on me2 response I wrote a category for MKMapView to add some margins and skip user location annotation:
@interface MKMapView (ZoomToFitAnnotations)
- (void)zoomToFitAnnotations:(BOOL)animated;
@end
@implementation MKMapView (ZoomToFitAnnotations)
- (void)zoomToFitAnnotations:(BOOL)animated {
if (self.annotations.count == 0)
return;
MKMapRect rect = MKMapRectNull;
for (id<MKAnnotation> annotation in self.annotations) {
if ([annotation isKindOfClass:[MKUserLocation class]] == false) {
MKMapPoint point = MKMapPointForCoordinate(annotation.coordinate);
rect = MKMapRectUnion(rect, MKMapRectMake(point.x, point.y, 0, 0));
}
}
MKCoordinateRegion region = MKCoordinateRegionForMapRect(rect);
region.span.longitudeDelta *= 2; // Margin
region.span.latitudeDelta *= 2; // Margin
[self setRegion:region animated:animated];
}
@end
Solution 22 - Ios
Since I can't comment on an answer, I would like to add my bit of convenience into @me2 's answer (since i thought it was the most elegant approach Found here).
For my personal project I simply added a category on the MKMapView class to encapsulate the "visible area" functionality for a ver common operation: setting to be able to see all the currently-loaded annotations on the MKMapView instance. the result was this:
.h file
#import <MapKit/MapKit.h>
@interface MKMapView (Extensions)
-(void)ij_setVisibleRectToFitAllLoadedAnnotationsAnimated:(BOOL)animated;
-(void)ij_setVisibleRectToFitAnnotations:(NSArray *)annotations animated:(BOOL)animated;
@end
.m file
#import "MKMapView+Extensions.h"
@implementation MKMapView (Extensions)
/**
* Changes the currently visible portion of the map to a region that best fits all the currently loadded annotations on the map, and it optionally animates the change.
*
* @param animated is the change should be perfomed with an animation.
*/
-(void)ij_setVisibleRectToFitAllLoadedAnnotationsAnimated:(BOOL)animated
{
MKMapView * mapView = self;
NSArray * annotations = mapView.annotations;
[self ij_setVisibleRectToFitAnnotations:annotations animated:animated];
}
/**
* Changes the currently visible portion of the map to a region that best fits the provided annotations array, and it optionally animates the change.
All elements from the array must conform to the <MKAnnotation> protocol in order to fetch the coordinates to compute the visible region of the map.
*
* @param annotations an array of elements conforming to the <MKAnnotation> protocol, holding the locations for which the visible portion of the map will be set.
* @param animated wether or not the change should be perfomed with an animation.
*/
-(void)ij_setVisibleRectToFitAnnotations:(NSArray *)annotations animated:(BOOL)animated
{
MKMapView * mapView = self;
MKMapRect r = MKMapRectNull;
for (id<MKAnnotation> a in annotations) {
ZAssert([a conformsToProtocol:@protocol(MKAnnotation)], @"ERROR: All elements of the array MUST conform to the MKAnnotation protocol. Element (%@) did not fulfill this requirement", a);
MKMapPoint p = MKMapPointForCoordinate(a.coordinate);
//MKMapRectUnion performs the union between 2 rects, returning a bigger rect containing both (or just one if the other is null). here we do it for rects without a size (points)
r = MKMapRectUnion(r, MKMapRectMake(p.x, p.y, 0, 0));
}
[mapView setVisibleMapRect:r animated:animated];
}
@end
As you can see, I've added 2 methods so far: one for setting the visible region of the map to the one that fits all currently-loaded annotations on the MKMapView instance, and another method to set it to any array of
//the mapView instance
[self.mapView ij_setVisibleRectToFitAllLoadedAnnotationsAnimated:animated];
I hope it helps =)
Solution 23 - Ios
Consider this extension:
extension MKCoordinateRegion {
init(locations: [CLLocationCoordinate2D], marginMultiplier: Double = 1.1) {
let mapRect = locations.reduce(MKMapRect(), {
let point = MKMapPointForCoordinate($1)
let rect = MKMapRect(origin: point, size: MKMapSize(width: 0.0, height: 0.0))
return MKMapRectUnion($0, rect)
})
var coordinateRegion = MKCoordinateRegionForMapRect(mapRect)
coordinateRegion.span.latitudeDelta *= marginMultiplier
coordinateRegion.span.longitudeDelta *= marginMultiplier
self = coordinateRegion
}
}
Solution 24 - Ios
A swift 5 version:
func regionFor(coordinates coords: [CLLocationCoordinate2D]) -> MKCoordinateRegion {
var r = MKMapRect.null
for i in 0 ..< coords.count {
let p = MKMapPoint(coords[i])
r = r.union(MKMapRect(x: p.x, y: p.y, width: 0, height: 0))
}
return MKCoordinateRegion(r)
}