Reputation: 12003
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
?
Upvotes: 98
Views: 83043
Reputation: 149
The easiest way:
mapView.layoutMargins = UIEdgeInsets(top: 20, left: 20, bottom: 20, right: 20)
mapView.showAnnotations(mapView.annotations, animated: true)
Upvotes: 0
Reputation: 20564
The link posted by Jim is now dead, but I was able to find the code (which I had bookmarked somewhere).
- (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];
}
Upvotes: 138
Reputation: 14169
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)
}
Upvotes: 3
Reputation: 692
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
}
Upvotes: 3
Reputation: 13563
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)
}
Upvotes: 0
Reputation: 861
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)
}
Upvotes: 5
Reputation: 221
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
}
}
Upvotes: 0
Reputation: 3332
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)
Upvotes: 5
Reputation: 2151
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)
}
Upvotes: 1
Reputation: 400
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)
}
Upvotes: 7
Reputation: 1599
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.
Upvotes: 4
Reputation: 21
- (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];
}
Upvotes: 2
Reputation: 844
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 objects. So to set the mapView's visible region the code would then be as simple as:
//the mapView instance
[self.mapView ij_setVisibleRectToFitAllLoadedAnnotationsAnimated:animated];
I hope it helps =)
Upvotes: 0
Reputation: 3466
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
Upvotes: 0
Reputation: 17280
As of iOS7 you can use showAnnotations:animated:
[mapView showAnnotations:annotations animated:YES];
Upvotes: 53
Reputation: 3953
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);
}
Upvotes: 139
Reputation: 2361
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];
Upvotes: 0
Reputation: 2571
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);
}
Upvotes: 0
Reputation: 2382
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
}
Upvotes: 1
Reputation: 2188
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])];
Upvotes: 5
Reputation: 1482
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];
}
Upvotes: 13
Reputation: 1358
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.
Upvotes: 20
Reputation: 4517
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;
}
Upvotes: 14
Reputation: 7778
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:
-(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];
}
Upvotes: 45
Reputation: 640
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.
Upvotes: 2