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.
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];
}
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;
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];
#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.
Related
Want to center MKMapView on a point N-pixels below a given pin (which may or may not be visible in the current MapRect).
I've been trying to solve this using various plays with -(CLLocationCoordinate2D)convertPoint:(CGPoint)point toCoordinateFromView:(UIView *)view to no success.
Anyone been down this road (no pun intended)?
The easiest technique is to just shift the map down, say 40% from where the coordinate would be, taking advantage of the span of the region of the MKMapView. If you don't need actual pixels, but just need it to move down so that the CLLocationCoordinate2D in question is near the top of the map (say 10% away from the top):
CLLocationCoordinate2D center = coordinate;
center.latitude -= self.mapView.region.span.latitudeDelta * 0.40;
[self.mapView setCenterCoordinate:center animated:YES];
If you want to account for rotation and pitch of the camera, the above technique may not be adequate. In that case, you could:
Identify the position in the view to which you want to shift the user location;
Convert that to a CLLocation;
Calculate the distance of the current user location from that new desired location;
Move the camera by that distance in the direction 180° from the current heading of the map's camera.
E.g. in Swift 3, something like:
var point = mapView.convert(mapView.centerCoordinate, toPointTo: view)
point.y -= offset
let coordinate = mapView.convert(point, toCoordinateFrom: view)
let offsetLocation = coordinate.location
let distance = mapView.centerCoordinate.location.distance(from: offsetLocation) / 1000.0
let camera = mapView.camera
let adjustedCenter = mapView.centerCoordinate.adjust(by: distance, at: camera.heading - 180.0)
camera.centerCoordinate = adjustedCenter
Where CLLocationCoordinate2D has the following extension:
extension CLLocationCoordinate2D {
var location: CLLocation {
return CLLocation(latitude: latitude, longitude: longitude)
}
private func radians(from degrees: CLLocationDegrees) -> Double {
return degrees * .pi / 180.0
}
private func degrees(from radians: Double) -> CLLocationDegrees {
return radians * 180.0 / .pi
}
func adjust(by distance: CLLocationDistance, at bearing: CLLocationDegrees) -> CLLocationCoordinate2D {
let distanceRadians = distance / 6_371.0 // 6,371 = Earth's radius in km
let bearingRadians = radians(from: bearing)
let fromLatRadians = radians(from: latitude)
let fromLonRadians = radians(from: longitude)
let toLatRadians = asin( sin(fromLatRadians) * cos(distanceRadians)
+ cos(fromLatRadians) * sin(distanceRadians) * cos(bearingRadians) )
var toLonRadians = fromLonRadians + atan2(sin(bearingRadians)
* sin(distanceRadians) * cos(fromLatRadians), cos(distanceRadians)
- sin(fromLatRadians) * sin(toLatRadians))
// adjust toLonRadians to be in the range -180 to +180...
toLonRadians = fmod((toLonRadians + 3.0 * .pi), (2.0 * .pi)) - .pi
let result = CLLocationCoordinate2D(latitude: degrees(from: toLatRadians), longitude: degrees(from: toLonRadians))
return result
}
}
So, even with the camera pitched and at a heading other than due north, this moves the user's location (which is centered, where the lower crosshair is) up 150 pixels (where the upper crosshair is), yielding something like:
Obviously, you should be conscious about degenerate situations (e.g. you're 1 km from the south pole and you try to shift the map up 2 km meters; you're using a camera angle pitched so far that the desired screen location is past the horizon; etc.), but for practical, real-world scenarios, something like the above might be sufficient. Obviously, if you don't let the user change the pitch of the camera, the answer is even easier.
Original answer: for moving the annotation n pixels
If you have a CLLocationCoordinate2D, you can convert it to a CGPoint, move it x pixels, and then convert it back to a CLLocationCoordinate2D:
- (void)moveCenterByOffset:(CGPoint)offset from:(CLLocationCoordinate2D)coordinate
{
CGPoint point = [self.mapView convertCoordinate:coordinate toPointToView:self.mapView];
point.x += offset.x;
point.y += offset.y;
CLLocationCoordinate2D center = [self.mapView convertPoint:point toCoordinateFromView:self.mapView];
[self.mapView setCenterCoordinate:center animated:YES];
}
You can call this by:
[self moveCenterByOffset:CGPointMake(0, 100) from:coordinate];
Unfortunately, this only works if the coordinate is visible before you start, so you might have to go to the original coordinate first, and then adjust the center.
For Swift:
import MapKit
extension MKMapView {
func moveCenterByOffSet(offSet: CGPoint, coordinate: CLLocationCoordinate2D) {
var point = self.convert(coordinate, toPointTo: self)
point.x += offSet.x
point.y += offSet.y
let center = self.convert(point, toCoordinateFrom: self)
self.setCenter(center, animated: true)
}
func centerCoordinateByOffSet(offSet: CGPoint) -> CLLocationCoordinate2D {
var point = self.center
point.x += offSet.x
point.y += offSet.y
return self.convert(point, toCoordinateFrom: self)
}
}
The only way to reliably do this is to use the following:
- (void)setVisibleMapRect:(MKMapRect)mapRect edgePadding:(UIEdgeInsets)insets animated:(BOOL)animate
In order to do that given a map region you want to center on, you have to convert the map region to a MKMapRect. Use the edge padding for the pixel offset, obviously.
See here for that:
Convert MKCoordinateRegion to MKMapRect
Comment: I find it rather strange that that's the only way to do it, given that MKMapRect is not something one normally uses with an MKMapView - all the conversion methods are for MKMapRegion. But, ok, at least it works. Tested in my own project.
One easy solution is that you make the frame of your map view larger than the visible area. Then position your pin in the center of the map view and hide all the unwanted areas behind another view or outside of the screen bounds.
Let me elaborate. If I look at your screen shot, do the following:
The distance between you pin and the bottom is 353 pixel. So make your map views frame twice the height: 706 pixel. You screenshot has a height of 411 pixel. Position your frame at an origin of 706px - 411px = -293 pixel.
Now center your map view at the coordinate of the pin and you are done.
Update 4-March-2014:
I created a small sample application with Xcode 5.0.2 to demo this: http://cl.ly/0e2v0u3G2q1d
SWIFT 3 UPDATED
Updated the function with Zoom
func zoomToPos() {
let span = MKCoordinateSpan(latitudeDelta: 0.1, longitudeDelta: 0.1)
// Create a new MKMapRegion with the new span, using the center we want.
let coordinate = moveCenterByOffset(offset: CGPoint(x: 0, y: 100), coordinate: (officeDetail?.coordinate)!)
let region = MKCoordinateRegion(center: coordinate, span: span)
mapView.setRegion(region, animated: true)
}
func moveCenterByOffset (offset: CGPoint, coordinate: CLLocationCoordinate2D) -> CLLocationCoordinate2D {
var point = self.mapView.convert(coordinate, toPointTo: self.mapView)
point.x += offset.x
point.y += offset.y
return self.mapView.convert(point, toCoordinateFrom: self.mapView)
}
after reading this thread ad playing around, especially with the zooming on annotation, I ended up with following procedures:
** Centering on annotation:**
- (void) centerOnSelection:(id<MKAnnotation>)annotation
{
MKCoordinateRegion region = self.mapView.region;
region.center = annotation.coordinate;
CGFloat per = ([self sizeOfBottom] - [self sizeOfTop]) / (2 * self.mapView.frame.size.height);
region.center.latitude -= self.mapView.region.span.latitudeDelta * per;
[self.mapView setRegion:region animated:YES];
}
** Zooming on annotation:**
- (void) zoomAndCenterOnSelection:(id<MKAnnotation>)annotation
{
DLog(#"zoomAndCenterOnSelection");
MKCoordinateRegion region = self.mapView.region;
MKCoordinateSpan span = MKCoordinateSpanMake(0.005, 0.005);
region.center = annotation.coordinate;
CGFloat per = ([self sizeOfBottom] - [self sizeOfTop]) / (2 * self.mapView.frame.size.height);
region.center.latitude -= self.mapView.region.span.latitudeDelta * span.latitudeDelta / region.span.latitudeDelta * per;
region.span = span;
[self.mapView setRegion:region animated:YES];
}
-(CGFloat) sizeOfBottom and -(CGFloat) sizeOfTop both return height of panels covering the mapview from the layout guides
As an alternative to the accepted answer, I suggest your original instincts were correct. You can work strictly within the map views pixel coordinate space to get offsets and final positioning. Then using the conversion call from location to screen view, you can get the final location and set the maps center.
This will work with the camera rotated and is in respect to the screen space. In my case I needed to center the map on a pin, with an offset to account for a map drawer.
Here are the conversion calls
func convert(_ coordinate: CLLocationCoordinate2D, toPointTo view: UIView?) -> CGPoint
func convert(_ point: CGPoint, toCoordinateFrom view: UIView?) -> CLLocationCoordinate2D
And here is a swift 4 example
//First get the position you want the pin to be (say 1/4 of the way up the screen)
let targetPoint = CGPoint(x: self.frame.width / 2.0, y: self.frame.height * CGFloat(0.25))
//Then get the center of the screen (this is used for calculating the offset as we are using setCenter to move the region
let centerPoint = CGPoint(x: self.frame.width / 2.0, y: self.frame.height / 2.0)
//Get convert the CLLocationCoordinate2D of the pin (or map location) to a screen space CGPoint
let annotationPoint = mapview.convert(myPinCoordinate, toPointTo: mapview)
//And finally do the math to set the offsets in screen space
let mapViewPointFromAnnotation = CGPoint(x: annotationPoint.x + (centerPoint.x - targetPoint.x), y: annotationPoint.y + (centerPoint.y - targetPoint.y))
//Now convert that result to a Coordinate
let finalLocation = self.convert(mapViewPointFromAnnotation, toCoordinateFrom: mapview)
//And set the map center
mapview.setCenter(finalLocation, animated: true)
Look at this method on MKMapView:
- (void)setCenterCoordinate:(CLLocationCoordinate2D)coordinate animated:(BOOL)animated
I'm trying to see if some extents (max x, max y, min x, min y coordinates) I have are in the current visible map view.
I take my extents and create a MKMapRect:
MKMapPoint upperLeft = MKMapPointForCoordinate(CLLocationCoordinate2DMake([boundary.extents.maxY floatValue], [boundary.extents.minY floatValue]));
MKMapPoint lowerLeft = MKMapPointForCoordinate(CLLocationCoordinate2DMake([boundary.extents.minY floatValue], [boundary.extents.minY floatValue]));
MKMapPoint upperRight = MKMapPointForCoordinate(CLLocationCoordinate2DMake([boundary.extents.maxY floatValue], [boundary.extents.maxY floatValue]));
MKMapRect mapRect = MKMapRectMake(upperLeft.x, upperLeft.y, fabs(upperLeft.x - upperRight.x), fabs(upperLeft.y - lowerLeft.y));
Now I want to check if my 'mapRect' is in the mapView.visibleMapRect:
if (MKMapRectContainsRect(mapView.visibleMapRect, mapRect)) {
// do some stuff
}
But my extents are never contained by the mapView.visibleMapRect when I know they should be.
If I replace the mapView.visibleMapRect with MKMapRectWorld, then it will contain my extents 'mapRect'.
Am I doing something wrong? is mapView.visibleMapRect not what I think it is (the viewable area on the screen)?
D'oh!
The issue was that I used minY instead of minX.
MKMapPoint upperLeft = MKMapPointForCoordinate(CLLocationCoordinate2DMake([boundary.extents.maxY floatValue], [boundary.extents.**minX** floatValue]));
MKMapPoint lowerLeft = MKMapPointForCoordinate(CLLocationCoordinate2DMake([boundary.extents.minY floatValue], [boundary.extents.**minX** floatValue]));
MKMapPoint upperRight = MKMapPointForCoordinate(CLLocationCoordinate2DMake([boundary.extents.maxY floatValue], [boundary.extents.**maxX** floatValue]));
mapView.visibleMapRect is exactly what you think it is, the map rect displayed by your map view. The problem is probably that the MKMapRectContainsRect function only tells you if one map rect is entirely contained (completely enclosed) in another. It's likely that you just want to use MKMapRectIntersectsRect which just tells you that part of your map rect is inside your mapView.visibleMapRect
I got MKMapView and a number of annotations on it. I use next code for displaying all annotations:
NSArray *coordinates = [self.mapView valueForKeyPath:#"annotations.coordinate"];
CLLocationCoordinate2D maxCoord = {-90.0f, -180.0f};
CLLocationCoordinate2D minCoord = {90.0f, 180.0f};
for(NSValue *value in coordinates) {
CLLocationCoordinate2D coord = {0.0f, 0.0f};
[value getValue:&coord];
if(coord.longitude > maxCoord.longitude) {
maxCoord.longitude = coord.longitude;
}
if(coord.latitude > maxCoord.latitude) {
maxCoord.latitude = coord.latitude;
}
if(coord.longitude < minCoord.longitude) {
minCoord.longitude = coord.longitude;
}
if(coord.latitude < minCoord.latitude) {
minCoord.latitude = coord.latitude;
}
}
MKCoordinateRegion region = {{0.0f, 0.0f}, {0.0f, 0.0f}};
region.center.longitude = (minCoord.longitude + maxCoord.longitude) / 2.0;
region.center.latitude = (minCoord.latitude + maxCoord.latitude) / 2.0;
region.span.longitudeDelta = (maxCoord.longitude - minCoord.longitude) * 1.5;
region.span.latitudeDelta = (maxCoord.latitude - minCoord.latitude) * 1.5;
[self.mapView setRegion:region animated:YES];
This code centers all annotations - perfect. But what if I want to fit annotations, lets say, on right half part of map. Is there any way to do that?
Here how it's now:
Here what I want to achieve:
If your target area is always to the left or right (i.e. still allowing full height) then it is very easy, you just need to work out the maths for your region's boundaries.
Let's assume you want the pins to be in the right-hand quarter of the page. This would mean you need to tell the map to view a region 4x (targetspan/currentspan) bigger than the pin region and with the middle of that region being shifted left by 1.5x ((targetspan-currentspan)/2)the width of the region. (I'm going to sat latitude is from 0 to 1 but really you just use what ever you have calculated already)
Imagine the view being divided into four vertical strips, there would be 5 lines. The left edge of the screen is 0, the next line is 1, the middle is 2, the next is 3 and the last is 4.
You want your pins to be between lines 3 and 4, meaning the longitude span you calculated above is 1 unit wide and the center.longitude is at 3.5.
You have a region that is ((3.5,0.5), (1,1)) and you need to tell your map to zoom to the region ((2,0.5),(4,1)). So, get the longitudeDelta, multiply it by 1.5 and subtract that from the center.longitude, that is your new target center.longitude. Then multiply the longitudeDelta by 4 and that is your new longitudeDelta.
You can do something similar with latitude on a small scale, but on a large scale the calculations ned to take into account the map projection which means the pixels near the equator represent more land than those near the poles.
I'm displaying an MKMapView inside a Path-style parallax table view header. To create the effect, the mapView bounds is larger than the area visible to the user. I need to set the map view region such that all the map's annotations are contained within the visible rect of MKMapView. What's the best way to do this?
Edit for clarity: Here's a use-case. The mapView size is 320 x 380. The visible area, however, is defined by the rect (0.0, 20.0, 320.0, 100.0). I need to set the region such that all the annotations appear in this rect within the mapView.
Setting the map region so that all annotations are contained in a certain part of an MKMapView can be done in three steps. Input are the mapView and the annotationsFrame.
Calculate an MKMapRect mapRect that contains all annotations.
Calculate the padding insets from mapView.bounds and annotationsFrame.
Call -setVisibleMapRect:edgePadding:animated: on the map view.
Below is a screen shot of a test. The red overlay shows the annotationsFrame.
Here is the code. Beware: It's all in one method to simplify adding it to your code, and it is not tested for edge cases like passing in n annotations with the same coordinate, or having the annotations so far apart that the map would have to get zoomed out too much, or having coordinates that span the edge of the map at +/-180 degrees longitude.
- (void)zoomAnnotationsOnMapView:(MKMapView *)mapView toFrame:(CGRect)annotationsFrame animated:(BOOL)animated
{
if (_mapView.annotations.count < 2) return;
// Step 1: make an MKMapRect that contains all the annotations
NSArray *annotations = _mapView.annotations;
id <MKAnnotation> firstAnnotation = [annotations objectAtIndex:0];
MKMapPoint minPoint = MKMapPointForCoordinate(firstAnnotation.coordinate);
MKMapPoint maxPoint = minPoint;
for (id <MKAnnotation> annotation in annotations) {
MKMapPoint point = MKMapPointForCoordinate(annotation.coordinate);
if (point.x < minPoint.x) minPoint.x = point.x;
if (point.y < minPoint.y) minPoint.y = point.y;
if (point.x > maxPoint.x) maxPoint.x = point.x;
if (point.y > maxPoint.y) maxPoint.y = point.y;
}
MKMapRect mapRect = MKMapRectMake(minPoint.x, minPoint.y, maxPoint.x - minPoint.x, maxPoint.y - minPoint.y);
// Step 2: Calculate the edge padding
UIEdgeInsets edgePadding = UIEdgeInsetsMake(
CGRectGetMinY(annotationsFrame),
CGRectGetMinX(annotationsFrame),
CGRectGetMaxY(mapBounds) - CGRectGetMaxY(annotationsFrame),
CGRectGetMaxX(mapBounds) - CGRectGetMaxX(annotationsFrame)
);
// Step 3: Set the map rect
[mapView setVisibleMapRect:mapRect edgePadding:edgePadding animated:animated];
}
If you go for a perfect placement (and who doesn't), here are three things to consider:
The code assures that all the coordinates are in the annotationsFrame, but the annotations themselves may be outside. To prevent that, simply use more padding. For example, if your annotations are 20x20 and centered on the coordinate, use 10 more padding on all sides.
Below iOS 7, the map was not zooming to the perfect zoom scale, but to the next tile size (power of two). So there will be more space around the annotations than needed, just as shown on the screenshot.
On iOS 7, the map view will not only zoom perfectly, but automatically care about the status bar. To make the calculation correct, you need to subtract the status bar height from the top padding on iOS 7.
Starting from iOS 7.0, this can be easily achieved with showAnnotations.
Swift:
mapView.showAnnotations(mapView.annotations, animated: true)
Objective-C:
[mapView showAnnotations:mapView.annotations animated:YES];
The above statement will adjust the map view's visible rect in order to display all annotations.
You first need to add the annotations:
(of course this is after you already have a list of annotations)
Swift4:
self.mapView.addAnnotations(annotations)
let currentView = mapView.visibleMapRect
mapView.annotations(in: currentView)
You can use the currentView constant or directly place the MKMapRect as such: Below: (.visibleMapRect returns:
"The area currently displayed by the map view."
mapView.annotations(in: mapView.visibleMapRect)
I found an easier way without calculating is let the map view calculate it, then we adjust the edges.
//1: Show all annotation on the map view, but without animation
self.mapView.showAnnotations(self.mapView.annotations, animated: false)
//2: Get the current visible map rect
let currentMapRect = self.mapView.visibleMapRect
//3: Create the edges inset that you want the map view to show all annotation within
let padding = UIEdgeInsets(top: 100, left: 100, bottom: 100, right: 100)
//4: Set current map rect but with new padding, also set animation to true to see effect
self.mapView.setVisibleMapRect(currentMapRect, edgePadding: padding, animated: true)
If you're prepared to approximate the calculations you can do it using some clever scaling.
Your target area is 80 tall out of a mapView that is 380. Therefore you want a region that is 4.75x taller than the region calculated to fit your annotations. (0.25 extra above and 3.5 extra below).
First you need to get a region (or maprect, what ever you prefer working in) and make it the same proportions as your target viewable area. This is because a really wide and short region would not be touching the top and bottom of the viewable area and therefore multiplying its height would not make something that touched the top and bottom of your map view. So if viewable_height/viewable_width > annotations_height/annotations_width you should set the annotations_height to annotations_width * (viewable_height/viewable_width).
With that you then add 25% on to the north of the annotations box and 350% on to the south. You can do this by moving the center 212.5% (of the current height) south and increasing the vertical span by 475%.
Now, all of this is an approximation given that the world is sphere and we're not looking at a planar projection (i.e. 1 degree of latitude near the equator is drawn smaller than 1 degree near the poles). But if you wally want to be accurate you could look into scaling the numbers according to latitude and such. If you're only dealing with annotations on a city-sized scale you'll probably be ok.
Hope that helps.
if you want to find the annotations that are in a given rect:
- (NSArray*)allAnnotationsInMapRect:(MKMapRect)mapRect {
NSMutableArray *annotationsInRect = [NSMutableArray array];
for(id<MKAnnotation *ann in self.allAnnotations) {
MKMapPoint pt = MKMapPointForCoordinate(ann.coordinate);
if(MKMapRectContainsPoint(mapRect, pt)) {
[annotationsInRect addObject:ann];
}
}
return annotationsInRect;
}
and to assure the annotation VIEWS are in the rect, get the region for the annotations,
then walk through them and get each view's bounds see if the bounds fit inside the visibleRect of the map and if not modify the region!
~~ like this:
- (void)assureAnnotationViewsAreVisible:(NSArray*)annotations originalRegion:(MKCoordinateRegion)originalRegion {
CGFloat smallestX = MAXFLOAT;
CGFloat smallestY = MAXFLOAT;
CGFloat biggestX = -100;
CGFloat biggestY = -100;
//NSLog(#"---: %d", annotations.count);
for(id<MKAnnotation> *annotation in annotations) {
UIView *annotationView = [self.mapView viewForAnnotation:v];
CGRect annotationViewFrame = annotationView.bounds;
annotationViewFrame.origin = [self.mapView convertCoordinate:annotationView.coordinate toPointToView:self.mapView];
annotationViewFrame.origin = CGPointMake(annotationViewFrame.origin.x-annotationViewFrame.size.width/2,
annotationViewFrame.origin.y-annotationViewFrame.size.height);
smallestX = MIN(annotationViewFrame.origin.x, smallestX);
smallestY = MIN(annotationViewFrame.origin.y, smallestY);
biggestX = MAX(annotationViewFrame.origin.x+annotationViewFrame.size.width, biggestX);
biggestY = MAX(annotationViewFrame.origin.y+annotationViewFrame.size.height, biggestY);
}
//NSLog(#"---");
CGRect bounds = self.mapView.bounds;
if(smallestX < bounds.origin.x || smallestY < bounds.origin.y || biggestX > bounds.origin.x+bounds.size.width || biggestY > bounds.origin.y+bounds.size.height) {
CGRect neededRect = bounds;
neededRect.origin = CGPointMake(MIN(bounds.origin.x, smallestX), MIN(bounds.origin.y, smallestY));
neededRect.size = CGSizeMake(MAX(bounds.size.width, biggestX), MAX(bounds.size.height, biggestY));
MKCoordinateRegion neededRegion = [self.mapView convertRect:neededRect toRegionFromView:self.mapView];
_ignoreRegionChange = YES;
[self.mapView setRegion:originalRegion animated:NO];
_ignoreRegionChange = NO;
[self.mapView setRegion:neededRegion animated:YES];
}
else {
MKCoordinateRegion currentRegion = self.mapView.region;
_ignoreRegionChange = YES;
[self.mapView setRegion:originalRegion animated:NO];
_ignoreRegionChange = NO;
[self.mapView setRegion:currentRegion animated:YES];
}
}
Try to get from all your annotation edges value (max and min) for lan and lon.
Define this value on the beginning:
static float maxLat = FLT_MIN;
static float maxLon = FLT_MIN;
static float minLat = FLT_MAX;
static float minLon = FLT_MAX;
and then use this function to calculate span and region:
- (void) zoomAndFit {
for(int i = 0; i < [self.points count]; i++) {
PRPlaceModel *place = [self.points objectAtIndex:i];
CLLocationCoordinate2D location;
location.latitude = [place.lat floatValue];
location.longitude = [place.lon floatValue];
minLat = MIN(minLat, location.latitude);
minLon = MIN(minLon, location.longitude);
maxLat = MAX(maxLat, location.latitude);
maxLon = MAX(maxLon, location.longitude);
}
MKCoordinateRegion region;
MKCoordinateSpan span;
span.latitudeDelta = 1.2*(maxLat - minLat);
span.longitudeDelta = 1.2*(maxLon - minLon);
CLLocationCoordinate2D location;
location.latitude = (minLat + maxLat)/2;
location.longitude = (minLon + maxLon)/2;
region.span=span;
region.center=location;
[self.mapView setRegion:region animated:TRUE];
[self.mapView regionThatFits:region];
}
And use it in viewDidLoad method:
-(void)viewDidLoad
{
[super viewDidLoad];
[self zoomAndFit];
}
I need to fit a certain bounds within a map. I get the bounds from calling the google geocoder and reading the viewport property which looks like:
{
northeast = {
lat = "30.4212235";
lng = "-97.486942";
};
southwest = {
lat = "30.1128403";
lng = "-97.99917959999999";
};
}
I then convert these into CLLocationCoordinate2D
NSDictionary *viewport = [[[results objectAtIndex:0] objectForKey:#"geometry"]
objectForKey:#"viewport"];
NSDictionary *NEDictionary = [viewport objectForKey:#"northeast"];
NSDictionary *SWDictionary = [viewport objectForKey:#"southwest"];
CLLocationCoordinate2D SWCoordinate =
CLLocationCoordinate2DMake(
[[SWDictionary objectForKey:#"lat"] floatValue],
[[SWDictionary objectForKey:#"lng"] floatValue]
);
CLLocationCoordinate2D NECoordinate =
CLLocationCoordinate2DMake(
[[NEDictionary objectForKey:#"lat"] floatValue],
[[NEDictionary objectForKey:#"lng"] floatValue]
);
I know I need to generate a MKMapRect (or MKMapRegion, whichever is easier) from these coordinates and then [mapView setVisibleRect:newRect animated:YES] (or [mapView setRegion:newRegion animated:YES] but I'm not quite sure how to get there. I need a method to convert the bounds into the proper data structure, something like:
- (MKMapRect) mapRectThatFitsBoundsSW:(CLLocationCoordinate2D)sw
NE:(CLLocationCoordinate2D)ne {
// CGFloat x = ??
// CGFloat y = ??
// CGFloat width = ??
// CGFloat height = ??
MKMapRect mapRectFromBounds = MKMapRectMake(x,y,width,height);
return mapRectFromBounds;
}
Any thoughts?
There are several ways to do this.
You could create an MKCoordinateRegion by figuring out the center point and then the span is the absolute difference in degrees between the corners.
Or you could create an MKMapRect by using the MapKit function MKMapPointForCoordinate. To get the origin, figure out the northwest coordinate and convert it to an MKMapPoint. To get the width and height, get the absolute difference in mappoints between the corners (convert the corners from coordinates to MKMapPoints using the function first).
Another quick way is a slight trick using the MKMapRectUnion function. Create a zero-size MKMapRect from each coordinate and then merge the two rects into one big rect using the function:
MKMapPoint swPoint = MKMapPointForCoordinate(SWCoordinate);
MKMapRect swRect = MKMapRectMake(swPoint.x, swPoint.y, 0, 0);
MKMapPoint nePoint = MKMapPointForCoordinate(NECoordinate);
MKMapRect neRect = MKMapRectMake(nePoint.x, nePoint.y, 0, 0);
MKMapRect rect = MKMapRectUnion(swRect, neRect);
Remember that the map view will still make its own adjustments to the rect you request based on the proportions of the map view and the required zoom. (If you want to know what that adjusted rect will be, call the map view's mapRectThatFits: method.)
If your bounds can span the 180th meridian, you have to account for that in the conversion:
- (MKMapRect) mapRectThatFitsBoundsSW:(CLLocationCoordinate2D)sw
NE:(CLLocationCoordinate2D)ne
{
MKMapPoint pSW = MKMapPointForCoordinate(sw);
MKMapPoint pNE = MKMapPointForCoordinate(ne);
double antimeridianOveflow =
(ne.longitude > sw.longitude) ? 0 : MKMapSizeWorld.width;
return MKMapRectMake(pSW.x, pNE.y,
(pNE.x - pSW.x) + antimeridianOveflow,
(pSW.y - pNE.y));
}
But beware those MKMapRects that span the anitmeridian, because they come from the land where the dragons live. If you want to learn about some of the dangers that lie there, have a look at
MKMapRect and displaying map overlays that span 180th meridian. You have been warned!
I found something that works. I ended up going with this:
- (MKMapRect) mapRectThatFitsBoundsSW:(CLLocationCoordinate2D)sw
NE:(CLLocationCoordinate2D)ne {
MKMapPoint nePoint = MKMapPointForCoordinate(ne);
MKMapPoint swPoint = MKMapPointForCoordinate(sw);
CGFloat width = ABS(nePoint.x - swPoint.x);
CGFloat height = ABS(nePoint.y - swPoint.y);
MKMapRect newMapRect = MKMapRectMake(
MIN(swPoint.x, nePoint.x),
MIN(swPoint.y, nePoint.y),
width,
height
);
// if (!MKMapRectSpans180thMeridian(newMapRect)) {
return newMapRect;
// } else {
// ????
// }
}