定位 MKMapView 以一次显示多个注释

我有几个注释要添加到 MKMapView 中(它可以是0-n 条目,其中 n 通常在5左右)。我可以很好地添加注释,但是我想调整地图的大小,以便一次性将所有注释放到屏幕上,而且我不确定如何做到这一点。

我一直在看 -regionThatFits:,但我不太确定该怎么处理它。我将发布一些代码来显示我目前已经得到了什么。我认为这应该是一个通常直接的任务,但我感到有点不知所措与 MapKit 到目前为止。

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

注意,这一切都发生在我收到一个位置更新... 我不知道这是否是一个合适的地方这样做。如果没有,哪里会是一个更好的地方?-viewDidLoad

先谢谢你。

81278 次浏览

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.

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

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

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

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.

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

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

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
}

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

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

As of iOS7 you can use showAnnotations:animated:

[mapView showAnnotations:annotations animated:YES];

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

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 =)

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

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)


}

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.

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)
}

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)
}

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)


}

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)

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

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)
}

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
}

enter image description here