One unit test affects another - ios

I'm defining two functions:
In mkgeometry_additions.h
#import <Foundation/Foundation.h>
#import <MapKit/MapKit.h>
MKMapRect MKMapRectForCoordinateRegion(MKCoordinateRegion region);
MKCoordinateRegion MKCoordinateRegionForSouthWestAndNorthEast(CLLocationCoordinate2D sw, CLLocationCoordinate2D ne);
In mkgeometry_additions.m (.m, even though they're c functions)
MKMapRect MKMapRectForCoordinateRegion(MKCoordinateRegion region)
{
CLLocationCoordinate2D center = region.center;
MKCoordinateSpan span = region.span;
CLLocationCoordinate2D topLeft =
CLLocationCoordinate2DMake(center.latitude - span.latitudeDelta / 2.0,
center.longitude - span.longitudeDelta / 2.0);
CLLocationCoordinate2D bottomRight =
CLLocationCoordinate2DMake(center.latitude - span.latitudeDelta / 2.0,
center.longitude - span.longitudeDelta / 2.0);
MKMapPoint mapPointTopLeft = MKMapPointForCoordinate(topLeft);
MKMapPoint mapPointBottomRight = MKMapPointForCoordinate(bottomRight);
double width = mapPointTopLeft.x - mapPointBottomRight.x;
double height = mapPointTopLeft.y - mapPointBottomRight.y;
return MKMapRectMake(mapPointTopLeft.x, mapPointTopLeft.y, width, height);
}
MKCoordinateRegion MKCoordinateRegionForSouthWestAndNorthEast(CLLocationCoordinate2D sw, CLLocationCoordinate2D ne)
{
MKCoordinateSpan span =
MKCoordinateSpanMake(ne.latitude - sw.latitude, ne.longitude - sw.longitude);
CLLocationCoordinate2D topLeft = CLLocationCoordinate2DMake(ne.latitude, sw.longitude);
CLLocationCoordinate2D center = CLLocationCoordinate2DMake(topLeft.latitude + span.latitudeDelta * 0.5, topLeft.longitude + span.longitudeDelta * 0.5);
return MKCoordinateRegionMake(center, span);
}
That's pretty much the entirety of the two files.
In my unit test with XCTests:
- (void)testMKMapRectForCoordinateRegionAfterReverseConversion
{
CLLocationCoordinate2D fremont = CLLocationCoordinate2DMake(37.54827, -121.98857);
MKCoordinateRegion region = MKCoordinateRegionMake(fremont, MKCoordinateSpanMake(0.05, 0.05));
MKMapRect mapRect = MKMapRectForCoordinateRegion(region);
MKCoordinateRegion derivedRegion = MKCoordinateRegionForMapRect(mapRect);
double accuracy = 0.0001;
XCTAssertEqualWithAccuracy(region.center.latitude, derivedRegion.center.latitude,
accuracy, #"Latitude is equal");
XCTAssertEqualWithAccuracy(region.center.longitude, derivedRegion.center.longitude,
accuracy, #"Latitude is equal");
XCTAssertEqualWithAccuracy(region.span.latitudeDelta, derivedRegion.span.latitudeDelta,
accuracy, #"Latitude delta is equal");
XCTAssertEqualWithAccuracy(region.span.longitudeDelta, derivedRegion.span.longitudeDelta,
accuracy, #"Latitude delta is equal");
}
- (void)testMKCoordinateRegionForSouthWestAndNorthEast
{
CLLocationCoordinate2D taipeiWanHua = CLLocationCoordinate2DMake(25.02946, 121.49652);
CLLocationCoordinate2D taipeiSongShan = CLLocationCoordinate2DMake(25.06640, 121.56166);
/* When the following line is called, the first test fails */
MKCoordinateRegionForSouthWestAndNorthEast(taipeiWanHua, taipeiSongShan);
/* I have the rest of the lines in this test commented out */
}
Now, I have empty setup and teardown for this test class. When I run the test. The first test fails if the third line of the second test is in the test. A closer look shows that when it fails, it's that the values are still similar but falling outside of the given accuracy:
If I commented that third line of the second test out, the first test would pass. Why is that?

The problem is not with unit testing - for me it fails even if I comment out entirely the second test - it is with the MKMapRectForCoordinateRegion implementation.
I think the correct implementation is:
MKMapRect MKMapRectForCoordinateRegion(MKCoordinateRegion region)
{
CLLocationCoordinate2D center = region.center;
MKCoordinateSpan span = region.span;
CLLocationCoordinate2D topLeft =
CLLocationCoordinate2DMake(center.latitude + span.latitudeDelta / 2.0,
center.longitude - span.longitudeDelta / 2.0);
CLLocationCoordinate2D bottomRight =
CLLocationCoordinate2DMake(center.latitude - span.latitudeDelta / 2.0,
center.longitude + span.longitudeDelta / 2.0);
MKMapPoint mapPointTopLeft = MKMapPointForCoordinate(topLeft);
MKMapPoint mapPointBottomRight = MKMapPointForCoordinate(bottomRight);
double width = mapPointBottomRight.x - mapPointTopLeft.x;
double height = mapPointBottomRight.y - mapPointTopLeft.y;
MKMapRect ret = MKMapRectMake(mapPointTopLeft.x, mapPointTopLeft.y, width, height);
return ret;
}
check out this: Mercator Projection

Related

Trying to get the span size in meters for an iOS MKCoordinateSpan

When I need to make an MKCoordinateRegion, I do the following:
var region = MKCoordinateRegion
.FromDistance(coordinate, RegionSizeInMeters, RegionSizeInMeters);
very simple - works perfectly.
Now I wish to store the value of the current region span. When i look at the region.Span value, it’s an MKCoordinateSpan which has two properties:
public double LatitudeDelta;
public double LongitudeDelta;
How can I convert the LatitudeDelta value into a latitudinalMeters please? (So then I can recreate my region (later on) using the above method...
As I can see you already have the region of the map. It doesn't only contain the lat & long deltas but also the center point of the region. You can calculate the distances in meters as illustrated in the picture:
1: Get the region span (how big the region is in lat/long degrees)
MKCoordinateSpan span = region.span;
2: Get the region center (lat/long coordinates)
CLLocationCoordinate2D center = region.center;
3: Create two locations (loc1 & loc2, north - south) based on the center location and calculate the distance inbetween (in meters)
//get latitude in meters
CLLocation *loc1 = [[CLLocation alloc] initWithLatitude:(center.latitude - span.latitudeDelta * 0.5) longitude:center.longitude];
CLLocation *loc2 = [[CLLocation alloc] initWithLatitude:(center.latitude + span.latitudeDelta * 0.5) longitude:center.longitude];
int metersLatitude = [loc1 distanceFromLocation:loc2];
4: Create two locations (loc3 & loc4, west - east) based on the center location and calculate the distance inbetween (in meters)
//get longitude in meters
CLLocation *loc3 = [[CLLocation alloc] initWithLatitude:center.latitude longitude:(center.longitude - span.longitudeDelta * 0.5)];
CLLocation *loc4 = [[CLLocation alloc] initWithLatitude:center.latitude longitude:(center.longitude + span.longitudeDelta * 0.5)];
int metersLongitude = [loc3 distanceFromLocation:loc4];
Swift implementation for Hanne's solution:
let span = mapView.region.span
let center = mapView.region.center
let loc1 = CLLocation(latitude: center.latitude - span.latitudeDelta * 0.5, longitude: center.longitude)
let loc2 = CLLocation(latitude: center.latitude + span.latitudeDelta * 0.5, longitude: center.longitude)
let loc3 = CLLocation(latitude: center.latitude, longitude: center.longitude - span.longitudeDelta * 0.5)
let loc4 = CLLocation(latitude: center.latitude, longitude: center.longitude + span.longitudeDelta * 0.5)
let metersInLatitude = loc1.distanceFromLocation(loc2)
let metersInLongitude = loc3.distanceFromLocation(loc4)
EDIT:
For Swift 5 distanceFromLocation(_:) has been renamed to distance(from:) meaning that the two last lines now read
let metersInLatitude = loc1.distance(from: loc2)
let metersInLongitude = loc3.distance(from: loc4)
Based on the above, here's a useful extension I use in my mapping projects:
extension MKCoordinateRegion {
public var radius: CLLocationDistance {
let loc1 = CLLocation(latitude: center.latitude - span.latitudeDelta * 0.5, longitude: center.longitude)
let loc2 = CLLocation(latitude: center.latitude + span.latitudeDelta * 0.5, longitude: center.longitude)
let loc3 = CLLocation(latitude: center.latitude, longitude: center.longitude - span.longitudeDelta * 0.5)
let loc4 = CLLocation(latitude: center.latitude, longitude: center.longitude + span.longitudeDelta * 0.5)
let metersInLatitude = loc1.distance(from: loc2)
let metersInLongitude = loc3.distance(from: loc4)
let radius = max(metersInLatitude, metersInLongitude) / 2.0
return radius
}
}

Convert GMSVisibleRegion to CLRegion or MKCoordinateRegion

I am using the GoogleMaps SDK and currently I am trying to convert a GMSVisibleRegion to a CLRegion.
GMSVisibleRegion is defined as:
typedef struct {
CLLocationCoordinate2D nearLeft;
CLLocationCoordinate2D nearRight;
CLLocationCoordinate2D farLeft;
CLLocationCoordinate2D farRight;
} GMSVisibleRegion;
What is the fastest way to do so?
Unfortunately it is difficult to understand what the developer meant with the naming "near" and "far". I think this comment can also be useful:
/**
* Returns the region (four location coordinates) that is visible according to
* the projection.
*
* The visible region can be non-rectangular. The result is undefined if the
* projection includes points that do not map to anywhere on the map (e.g.,
* camera sees outer space).
*/
- (GMSVisibleRegion)visibleRegion;
Thanks a lot!
EDIT:
Ok my first step was to create a MKCoordinateRegion of a GMSVisibleRegion.
I propose the following code to transform a a GMSVisibleRegion to a MKCoordinateRegion. Any objections.
+ (MKCoordinateRegion)regionForCenter:(CLLocationCoordinate2D)center andGMSVisibleRegion:(GMSVisibleRegion)visibleRegion
{
CLLocationDegrees latitudeDelta = visibleRegion.farLeft.latitude - visibleRegion.nearLeft.latitude;
CLLocationDegrees longitudeDelta = visibleRegion.farRight.longitude - visibleRegion.farLeft.longitude;
MKCoordinateSpan span = MKCoordinateSpanMake(latitudeDelta, longitudeDelta);
return MKCoordinateRegionMake(center, span);
}
My guess is that 'near' is for the corners of the view at the bottom of the screen, and 'far' is for the corners at the top of the screen. This is because if you've tilted the view, then the bottom corners are nearest to the camera, and the top corners are furthest from the camera.
One way to turn this into a CLRegion might be to use the camera's target as the centre, and then calculate the radius from the maximum distance to the four corners. This might not be the tightest fitting circle over the region, but since a circle can't fit the quadrilateral of the view anyway, it may be close enough.
Here's a helper function to calculate the distance in metres between two CLLocationCoordinate values:
double getDistanceMetresBetweenLocationCoordinates(
CLLocationCoordinate2D coord1,
CLLocationCoordinate2D coord2)
{
CLLocation* location1 =
[[CLLocation alloc]
initWithLatitude: coord1.latitude
longitude: coord1.longitude];
CLLocation* location2 =
[[CLLocation alloc]
initWithLatitude: coord2.latitude
longitude: coord2.longitude];
return [location1 distanceFromLocation: location2];
}
Then the CLRegion can be calculated like this:
GMSMapView* mapView = ...;
...
CLLocationCoordinate2D centre = mapView.camera.target;
GMSVisibleRegion* visibleRegion = mapView.projection.visibleRegion;
double nearLeftDistanceMetres =
getDistanceMetresBetweenLocationCoordinates(centre, visibleRegion.nearLeft);
double nearRightDistanceMetres =
getDistanceMetresBetweenLocationCoordinates(centre, visibleRegion.nearRight);
double farLeftDistanceMetres =
getDistanceMetresBetweenLocationCoordinates(centre, visibleRegion.farLeft);
double farRightDistanceMetres =
getDistanceMetresBetweenLocationCoordinates(centre, visibleRegion.farRight);
double radiusMetres =
MAX(nearLeftDistanceMetres,
MAX(nearRightDistanceMetres,
MAX(farLeftDistanceMetres, farRightDistanceMetres)));
CLRegion region = [[CLRegion alloc]
initCircularRegionWithCenter: centre radius: radius identifier: #"id"];
UPDATE:
Regarding your update for MKCoordinateRegion, your example code may not work. If the map has been rotated 90 degrees, then farLeft and nearLeft will have the same latitude, and farRight and farLeft will have the same longitude, and so your latitude and longitude deltas would be zero.
You would need to loop over all four of the farLeft, farRight, nearLeft, nearRight, calculate the min and max of the latitude and longitude of each, and then calculate the delta from that.
The Google Maps SDK for iOS includes a helper class which already does some of this for you - GMSCoordinateBounds. It can be initialized with a GMSVisibleRegion:
GMSMapView* mapView = ...;
....
GMSVisibleRegion visibleRegion = mapView.projection.visibleRegion;
GMSCoordinateBounds bounds =
[[GMSCoordinateBounds alloc] initWithRegion: visibleRegion];
The GMSCoordinateBounds then has northEast and southWest properties which define the bounds. So you could calculate the deltas as follows:
CLLocationDegrees latitudeDelta =
bounds.northEast.latitude - bounds.southWest.latitude;
CLLocationDegrees longitudeDelta =
bounds.northEast.longitude - bounds.southWest.longitude;
You could also calculate the centre from the bounds, and therefore the MKCoordinateRegion:
CLLocationCoordinate2D centre = CLLocationCoordinate2DMake(
(bounds.southWest.latitude + bounds.northEast.latitude) / 2,
(bounds.southWest.longitude + bounds.northEast.longitude) / 2);
MKCoordinateSpan span = MKCoordinateSpanMake(latitudeDelta, longitudeDelta);
return MKCoordinateRegionMake(centre, span);
Addendum for purists
If you want to be absolutely rigorous there's a correction you need to make around the international dateline. It would be a waste of effort in most apps but this problem has been causing me major grief as of late so I thought to throw it into the community hat
Building on Druce's update (afraid I can't make comments)...
GMSMapView* mapView = ...;
....
GMSVisibleRegion visibleRegion = mapView.projection.visibleRegion;
GMSCoordinateBounds bounds =
[[GMSCoordinateBounds alloc] initWithRegion: visibleRegion];
Latitude doesn't need anything doing to it
CLLocationDegrees latitudeDelta =
bounds.northEast.latitude - bounds.southWest.latitude;
The deal runs that a region that spans the international dateline might have a southWest corner in Japan (+140 longitude) and its northEast corner in Alaska (-150 longitude). Adding up and dividing by two gives a point around the wrong side of the globe.
The special case where northEast.longitude is less than southWest.longitude needs handling
CLLocationCoordinate2D centre;
CLLocationDegrees longitudeDelta;
if(bounds.northEast.longitude >= bounds.southWest.longitude) {
//Standard case
centre = CLLocationCoordinate2DMake(
(bounds.southWest.latitude + bounds.northEast.latitude) / 2,
(bounds.southWest.longitude + bounds.northEast.longitude) / 2);
longitudeDelta = bounds.northEast.longitude - bounds.southWest.longitude;
} else {
//Region spans the international dateline
centre = CLLocationCoordinate2DMake(
(bounds.southWest.latitude + bounds.northEast.latitude) / 2,
(bounds.southWest.longitude + bounds.northEast.longitude + 360) / 2);
longitudeDelta = bounds.northEast.longitude + 360
- bounds.southWest.longitude;
}
MKCoordinateSpan span = MKCoordinateSpanMake(latitudeDelta, longitudeDelta);
return MKCoordinateRegionMake(centre, span);
For anyone looking for boilerplate code based on all the answers and corrections provided so far, here's region implemented as a category on GMSMapView:
//
// GMSMapViewExtensions.h
//
#import <Foundation/Foundation.h>
#import <MapKit/MapKit.h>
#import <GoogleMaps/GoogleMaps.h>
#interface GMSMapView (GMSMapViewExtensions)
#end
and
//
// GMSMapViewExtensions.m
//
#import "GMSMapViewExtensions.h"
#implementation GMSMapView (GMSMapViewExtensions)
- (MKCoordinateRegion) region {
GMSVisibleRegion visibleRegion = self.projection.visibleRegion;
GMSCoordinateBounds * bounds = [[GMSCoordinateBounds alloc] initWithRegion: visibleRegion];
CLLocationDegrees latitudeDelta = bounds.northEast.latitude - bounds.southWest.latitude;
CLLocationCoordinate2D centre;
CLLocationDegrees longitudeDelta;
if (bounds.northEast.longitude >= bounds.southWest.longitude) {
// Standard case
centre = CLLocationCoordinate2DMake(
(bounds.southWest.latitude + bounds.northEast.latitude) / 2,
(bounds.southWest.longitude + bounds.northEast.longitude) / 2);
longitudeDelta = bounds.northEast.longitude - bounds.southWest.longitude;
} else {
// Region spans the international dateline
centre = CLLocationCoordinate2DMake(
(bounds.southWest.latitude + bounds.northEast.latitude) / 2,
(bounds.southWest.longitude + bounds.northEast.longitude + 360) / 2);
longitudeDelta = bounds.northEast.longitude + 360 - bounds.southWest.longitude;
}
MKCoordinateSpan span = MKCoordinateSpanMake(latitudeDelta, longitudeDelta);
return MKCoordinateRegionMake(centre, span);
}
- (MKMapRect)visibleMapRect {
MKCoordinateRegion region = [self region];
MKMapPoint a = MKMapPointForCoordinate(CLLocationCoordinate2DMake(
region.center.latitude + region.span.latitudeDelta / 2,
region.center.longitude - region.span.longitudeDelta / 2));
MKMapPoint b = MKMapPointForCoordinate(CLLocationCoordinate2DMake(
region.center.latitude - region.span.latitudeDelta / 2,
region.center.longitude + region.span.longitudeDelta / 2));
return MKMapRectMake(MIN(a.x, b.x), MIN(a.y, b.y), ABS(a.x - b.x), ABS(a.y - b.y));
}
#end
Usage example:
GMSMapView * mapView = .... // init code
MKCoordinateRegion mapRegion = mapView.region;
Based on answer of #Saxon Druce, this is a quick extension on setting and getting region using MKCoordinateRegion on GMSMapView
extension GMSMapView {
var region : MKCoordinateRegion {
get {
let position = self.camera
let visibleRegion = self.projection.visibleRegion()
let bounds = GMSCoordinateBounds(region: visibleRegion)
let latitudeDelta = bounds.northEast.latitude - bounds.southWest.latitude
let longitudeDelta = bounds.northEast.longitude - bounds.southWest.longitude
let center = CLLocationCoordinate2DMake(
(bounds.southWest.latitude + bounds.northEast.latitude) / 2,
(bounds.southWest.longitude + bounds.northEast.longitude) / 2)
let span = MKCoordinateSpanMake(latitudeDelta, longitudeDelta)
return MKCoordinateRegionMake(center, span)
}
set {
let northEast = CLLocationCoordinate2DMake(newValue.center.latitude - newValue.span.latitudeDelta/2, newValue.center.longitude - newValue.span.longitudeDelta/2)
let southWest = CLLocationCoordinate2DMake(newValue.center.latitude + newValue.span.latitudeDelta/2, newValue.center.longitude + newValue.span.longitudeDelta/2)
let bounds = GMSCoordinateBounds(coordinate: northEast, coordinate: southWest)
let update = GMSCameraUpdate.fitBounds(bounds, withPadding: 0)
self.moveCamera(update)
}
}
}

Zoom MKMapView to best fit distance zoom level based on list of locations

I have a center location I want my MKMapView to zoom to and also a list of locations (latitude/longitude). I would like to set the zoom level (distance) based on this list of locations so that as many of these locations are visible on the MKMapView at the same time, but with a minimum zoom level (or max distance) so that my MKMapView does not zoom out to much.
So lets say I have a list of 10 locations but one of those locations is so far away from my center location that if I show that one location on the MKMapView it will be zoomed out to much, how do I calculate the distance parameters for MKCoordinateRegionMakeWithDistance?
I hope I explained my problem well enough, I have difficulties explaining it I think.
Thank you
Søren
I got it working with the following solution.
I use the Great-circle distance formula to calculate the distance between my center point and all the found lat/long points, and if this distance is greater than my minimum distance and my maximum distance and larger than my "last found distance" I set this distance to be my zoom distance. It works like a charm and was actually quite simple.
Here is my distance calculation code:
-(double)distanceBetweenLocations:(CLLocationCoordinate2D)c1 :(CLLocationCoordinate2D)c2 {
int r = 6371 * 1000;//meters
double dLat = (c2.latitude - c1.latitude) * M_PI / 180;
double dLon = (c2.longitude - c1.longitude) * M_PI / 180;
double lat1 = c1.latitude * M_PI / 180;
double lat2 = c2.latitude * M_PI / 180;
double a = sin(dLat / 2) * sin(dLat / 2) + sin(dLon / 2) * sin(dLon / 2) * cos(lat1) * cos(lat2);
double c = 2 * atan2(sqrt(a), sqrt(1 - a));
double d = r * c;
return d;
}
And here is my code that zooms to my center point with the calculated distance:
-(void)zoomToLocation {
double MAX_DISTANCE = 10000.0;
double MIN_DISTANCE = 600.0;
double zoomDistance = MIN_DISTANCE;
CLLocationCoordinate2D center;
center.latitude = self.searchLocationLatitude;
center.longitude = self.searchLocationLongitude;
BOOL treaterVisible = NO;
for (Treater *treater in treaters) {
CLLocationCoordinate2D c2;
c2.latitude = treater.latitude;
c2.longitude = treater.longitude;
double distance = [self distanceBetweenLocations:center :c2];
if(distance > zoomDistance && distance < MAX_DISTANCE) {
zoomDistance = distance;
treaterVisible = YES;
}
}
if(!treaterVisible) {
zoomDistance = MAX_DISTANCE;
}
CLLocationCoordinate2D location;
location.latitude = self.searchLocationLatitude;
location.longitude = self.searchLocationLongitude;
MKCoordinateRegion region = MKCoordinateRegionMakeWithDistance(location, zoomDistance, zoomDistance);
MKCoordinateRegion adjustedRegion = [self.mapView regionThatFits:region];
[self.mapView setRegion:adjustedRegion];
}
Just if anybody should need something similar.
Best regards
Søren
Edit didAddAnnotationViews method. I hope this is what you are looking for,
-(void) mapView:(MKMapView *)mapView didAddAnnotationViews:(NSArray *)views
{
MKMapRect zoomRect = MKMapRectNull;
for (id <MKAnnotation> annotation in mapView.annotations)
{
MKMapPoint annotationPoint = MKMapPointForCoordinate(annotation.coordinate);
MKMapRect pointRect = MKMapRectMake(annotationPoint.x, annotationPoint.y, 0.1, 0.1);
if (MKMapRectIsNull(zoomRect)) {
zoomRect = pointRect;
} else {
zoomRect = MKMapRectUnion(zoomRect, pointRect);
}
}
[mapView setVisibleMapRect:zoomRect animated:YES];
}
DDAnnotation is custom class for display pin(PlaceMark)
-(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(DDAnnotation* 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);
}
NSLog(#"A%f, B%f, C%f, D%f,", topLeftCoord.latitude, topLeftCoord.longitude, bottomRightCoord.latitude, bottomRightCoord.longitude);
MKCoordinateRegion region;
region.center.latitude = topLeftCoord.latitude - (topLeftCoord.latitude - bottomRightCoord.latitude) * 0.5;
region.center.longitude = topLeftCoord.longitude + (bottomRightCoord.longitude - topLeftCoord.longitude) * 0.5;
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];
}
Some days ago i have read a solution to your problem in a book, I think it is Preparata Shamos, Computational Geometry.
Unfortunatley the solution is komplex: it is the question: find the best/max subset of points that are visible withing a given radius.
if your point count is not to high, less than ten thousands, i would do it as follows
0) add all point to current selected subset.
1) iterate through all points insubset, calcualte gravity point, calc all distances to gravity point, does it fit the max zoom level desired distance? yes the ready.
2) No? the remove the point from subset, with highest distance, and do step 1 again.
Do that till all points are within the desired distance from the current subset center.
The gravity point is that point in the subset with average x (or longitude) and average y ( or latitude) coordinate.
(If you accept, please dont upvote)
For this we need to find the minimum and maximum latitude and longitude, after that we need make the region with average sum of these latitude and longitude. then assign the region to the mapview and fit the region on it. thats it.

What's the best way to zoom out and fit all annotations in MapKit

Quick background of what I have going on. UIMapView loads and shows a single annotation (Never more than one). On the menu bar, there is a Locate Me button, on tap the userLocation is found and displayed as a second annotation. I then I have MapView zoom out to fit both those annotations in range but I am unable to find a suitable way of doing so. Depending on where the first annotation is placed in relation to the user location, sometimes it doesn't zoom out enough.
For example, if the annotation is North West of the User, it zooms out fine. But if the annotation is South East of the User, it only zooms out enough that they are just getting cut off and you have to manually zoom out a bit more. Here's the code I am using, variation of some others I have found on SO.
CLLocation *whereIAm = mapView.userLocation.location;
float lat = whereIAm.coordinate.latitude;
float lon = whereIAm.coordinate.longitude;
CLLocationCoordinate2D southWest = {[currentLatitude floatValue], [currentLongitude floatValue]};
CLLocationCoordinate2D northEast = southWest;
southWest.latitude = MIN(southWest.latitude, lat);
southWest.longitude = MIN(southWest.longitude, lon);
northEast.latitude = MAX(northEast.latitude, lat);
northEast.longitude = MAX(northEast.longitude, lon);
CLLocation *locSouthWest = [[CLLocation alloc] initWithLatitude:southWest.latitude longitude:southWest.longitude];
CLLocation *locNorthEast = [[CLLocation alloc] initWithLatitude:northEast.latitude longitude:northEast.longitude];
CLLocationDistance meters = [locSouthWest distanceFromLocation: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 = 7.0;
MKCoordinateRegion savedRegion = [mapView regionThatFits:region];
[mapView setRegion:savedRegion animated:YES];
[locSouthWest release];
[locNorthEast release];
Is there a better way built into MapKit to just zoom out so that both annotations have, lets say half an inch between them at the outer frame? I don't really care if the user has to zoom in after, I just want it to zoom out properly.
-(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(MapAnnotation* 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;
region.span.latitudeDelta = fabs(topLeftCoord.latitude - bottomRightCoord.latitude) * 1.1; // Add a little extra space on the sides
region.span.longitudeDelta = fabs(bottomRightCoord.longitude - topLeftCoord.longitude) * 1.1; // Add a little extra space on the sides
region = [mapView regionThatFits:region];
[mapView setRegion:region animated:YES];
}
Taken from: http://codisllc.com/blog/zoom-mkmapview-to-fit-annotations/
(Use it all the time.)
In iOS7 there's a method that does just that. Just call:
[yourMapView showAnnotations:yourAnnotationsArray animated:YES];
Those who do the monotouch c# coding
BasicMapAnnotation is inherit class from MKAnnotation
private void GetRegion(MKMapView mapView)
{
var userWasVisible = mapView.ShowsUserLocation;
mapView.ShowsUserLocation = false; // ignoring the blue blip
// start with the widest possible viewport
var tl = new CLLocationCoordinate2D(-90, 180); // top left
var br = new CLLocationCoordinate2D(90, -180); // bottom right
foreach (var an in mapView.Annotations)
{
// narrow the viewport bit-by-bit
CLLocationCoordinate2D coordinate = ((BasicMapAnnotation) an).Coordinate;
tl.Longitude = Math.Min(tl.Longitude, coordinate.Longitude);
tl.Latitude = Math.Max(tl.Latitude, coordinate.Latitude);
br.Longitude = Math.Max(br.Longitude, coordinate.Longitude);
br.Latitude = Math.Min(br.Latitude, coordinate.Latitude);
}
var center = new CLLocationCoordinate2D
{
// divide the range by two to get the center
Latitude = tl.Latitude - (tl.Latitude - br.Latitude)*0.5,Longitude = tl.Longitude + (br.Longitude - tl.Longitude)*0.5
};
var span = new MKCoordinateSpan
{
// calculate the span, with 20% margin so pins aren’t on the edge
LatitudeDelta = Math.Abs(tl.Latitude - br.Latitude)*1.2,LongitudeDelta = Math.Abs(br.Longitude - tl.Longitude)*1.2
};
var region = new MKCoordinateRegion {Center = center, Span = span};
region = mapView.RegionThatFits(region); // adjusts zoom level too
mapView.SetRegion(region, true); // animated transition
mapView.ShowsUserLocation =
userWasVisible;
}
You can use this code to show all annotations
MKMapRect zoomRect = MKMapRectNull;
for (id <MKAnnotation> annotation in mapView.annotations)
{
MKMapPoint annotationPoint = MKMapPointForCoordinate(annotation.coordinate);
MKMapRect pointRect = MKMapRectMake(annotationPoint.x, annotationPoint.y, 0.1, 0.1);
zoomRect = MKMapRectUnion(zoomRect, pointRect);
}
[mapView setVisibleMapRect:zoomRect animated:YES];
if you want to include user location just replace two lines below with the first line of above code
MKMapPoint annotationPoint = MKMapPointForCoordinate(mapView.userLocation.coordinate);
MKMapRect zoomRect = MKMapRectMake(annotationPoint.x, annotationPoint.y, 0.1, 0.1);

iPhone Map Region Weirdness

The attached image is what happens sometimes when I try to size a MKMapView to fit one or more placemarks. It's intermittent, but the display is always in exactly the same position.
Here is the code:
// loc1 is always non-null, and is equal to one of the annotation locations
CLLocationCoordinate2D topLeftCoord = loc1.coordinate;
CLLocationCoordinate2D bottomRightCoord = loc1.coordinate;
for(UserPlacemark* annotation in self.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;
double k = 0.01;
region.center.latitude = topLeftCoord.latitude - (topLeftCoord.latitude - bottomRightCoord.latitude) * 0.25;
region.center.longitude = topLeftCoord.longitude + (bottomRightCoord.longitude - topLeftCoord.longitude) * 0.5;
region.span.latitudeDelta = k + fabs(topLeftCoord.latitude - bottomRightCoord.latitude) * 1.25; // Add a little extra space on the sides
region.span.longitudeDelta = k + fabs(bottomRightCoord.longitude - topLeftCoord.longitude) * 1.5; // Add a little extra space on the sides
// only zoom if region doesn't fit, or too small
CGRect newRect = [mapView convertRegion:region toRectToView:mapView];
double MIN_SIZE_PIXELS = 50.0;
double rectSizePixels = newRect.size.width+newRect.size.height;
if (!CGRectContainsRect(mapView.bounds, newRect) || rectSizePixels < MIN_SIZE_PIXELS)
{
region = [mapView regionThatFits:region];
[mapView setRegion:region animated:TRUE];
}
The problem in the above code is that a MKUserAnnotation instance is placed in the placemark list by the Map framework. This does not always correspond with the user's actual position, at least in the simulator.
It's better to avoid iterating over the annotations list and instead compute the min/max boundaries of your placemark using your objects directly.

Resources