Why MKCoordinateSpan changes? - iphone

When I debug code below, I see that span is changed by mapkit from what I have provided. span2 contains different numbers from what was provided. Why is this happening?
- (void) viewDidLoad
{
[super viewDidLoad];
CLLocationCoordinate2D loc;
loc.latitude = self.atm.lat;
loc.longitude = self.atm.lon;
MKCoordinateSpan span1 = MKCoordinateSpanMake(0.05f, 0.05f);
self.mapView.region = MKCoordinateRegionMake(loc, span1);
// at this point numbers are not 0.05 anymore
MKCoordinateSpan span2 = self.mapView.region.span;
// ... more code
}

Note that latitude and longitude change differently so square MapView will actually display region with different span values for its coordinates. This is likely to cause your mapview region to have span (slightly?) different from what you set.
In reference for MKMapView's region property there's somewhat relevant phrase:
Changing only the center coordinate of the region can still
cause the span to change implicitly.
This is due to the fact that the
distances represented by a span change
at different latitudes and longitudes
and the map view may need to adjust
the span to account for the new
location.

A square MapView with identical values for the latitudinal and longitudinal span will almost always experience the change in span indicated above as longitudinal arc-length varies significantly as you move from the equator to either pole. As the latitude approaches +/-90, the longitudinal arc-length approaches 0.
In addition, however, identical span values will also change if the MapView region is not square (as the span only refers to the region actually visible at the time).

Related

MKCoordinateRegionMakeWithDistance gives wrong results on iphoneX

When setting the region for a MKMapView using MKCoordinateRegionMakeWithDistance, the resulting region always gives the wrong results, where the size is always slightly bigger than the best fit I would get for other phone models.
for example, doing:
let region = MKCoordinateRegionMakeWithDistance(someLocation, 400, 200)
let adjustedRegion = mapView.regionThatFits(region)
mapView.setRegion(adjustedRegion, animated: true)
(The mapview's vertical and horizontal ratios are defined to be set to 2:1)
would always result in a view that would give me 420 m vertically, 210~ m horizontally, while this doesn't happen for other phone models.
Understandably, it is meant to find the 'best fit` region for the specified dimensions, what's concerning me is that the results are different on iPhone X specifically. (on models 8, 8+, 5s)
Is there something I need to do specifically for iPhone X models with mapViews?
Turns out, mapkit's mapView's MKCoordinateRegionMakeWithDistance does it's calculations without the safe area insets.
Since my mapView was set to be at the bottom of the screen, when applying the vertical distance, some reduction needs to be made to compensate for this weird behaviour.
let verticalDistance = 400 * ((mapView.bounds.height - mapView.safeAreaInsets.bottom) / mapView.bounds.height )
let region = MKCoordinateRegionMakeWithDistance(someLocation, verticalDistance, 200)
let adjustedRegion = mapView.regionThatFits(region)
mapView.setRegion(adjustedRegion, animated: true)
This allow the mapView's resulting region to be correct in vertical and horizontal distance (compared against google map's web distance measuring tool)

Zoom MapView to show annotations across 180th meridian

I have a MapView with <5 annotations. When the map loads, I want to zoom in to center the map on these annotations, like this. However, the linked method doesn't work when the annotations are spread across the 180th meridian (where longitude wraps from -180 to +180). How can I robustly zoom to the annotations even if they are clustered around the prime meridian?
For example, if I have x(0,179) and y(0,-179) then I'd want a containing rect with a width of two degrees longitude, not 358.
MapKit for iOS actually has a built-in function in MKMapView which does this:
showAnnotations(annotations: [MKAnnotation], animated: Bool)
However, this function doesn't offer control over the zoom level, so I still had to make my own function. Here is the pseudocode:
zoomToAnnotations(annotations, zoomLimit) {
// calculate the midpoint as the average latitude and longitude
average_latitude = mean(a.coordinate.latitude for a in annotations)
average_longitude = atan2(mean(sin(a.coordinate.longitude) for a in annotations), mean(cos(a.coordinate.longitude) for a in annotations)) // be careful with degrees and radians in real code
regular_midpoint = coordinate(average_latitude, average_longitude)
// subtract 180 from longitude for the midpoint of the region crossing the meridian
meridian_midpoint = coordinate(average_latitude, average_longitude - 180.0)
if (meridian_midpoint.longitude < -180) {
meridian_midpoint.longitude += 360.0
}
// work out which region will be smaller and center map there
meridian_sum = sum(a.coordinate.distance(meridian_midpoint) for a in annotations)
regular_sum = sum(a.coordinate.distance(regular_midpoint) for a in annotations)
regular_max_distance = max(a.coordinate.distance(regular_midpoint) for a in annotations)
if meridian_sum < regular_sum {
meridian_max_distance = max(a.coordinate.distance(meridian_midpoint) for a in annotations)
centerMapWithRadius(meridian_midpoint, max(zoomLimit, meridian_max_distance))
} else {
regular_max_distance = max(a.coordinate.distance(regular_midpoint) for a in annotations)
centerMapWithRadius(regular_midpoint, max(zoomLimit, regular_max_distance))
}
}
This method isn't perfect , as it centers on the mean of the points instead of having the furthest points equal distances from the edge.

Searching CoreData with the maps visibleRegion

I have several thousand locations stored in CoreData and I would like to search for locations that are within a Google Maps visibleRegion. I was previously doing a search with a bounding box but the addition of the bearing feature breaks this type of query. I have several ideas but this must be a common problem with some well thought out solutions. I'd be interested to see if any solutions use geohashes.
This is my query that breaks when the bearing is not due north.
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"(lat > %f AND lat < %f AND lng > %f AND lng < %f)",
[self.googleMap.projection visibleRegion].nearLeft.latitude,
[self.googleMap.projection visibleRegion].farLeft.latitude,
[self.googleMap.projection visibleRegion].nearLeft.longitude,
[self.googleMap.projection visibleRegion].nearRight.longitude
];
You can calculate an axis-aligned bounding box of the visible region, and then use that to look up your locations. Some of them will still be outside of the actual visible area, but at least you'll filter out most of them.
The GMSCoordinateBounds class in GMSCoordinateBounds.h can be used to make this easier:
GMSMapView* _mapView = ...;
GMSCoordinateBounds* bounds =
[[GMSCoordinateBounds alloc]
initWithRegion: [_mapView.projection visibleRegion]];
CLLocationCoordinate2D northEast = bounds.northEast;
CLLocationCoordinate2D southWest = bounds.southWest;
Note also that there is currently a bug with visibleRegion being too large, see here:
https://code.google.com/p/gmaps-api-issues/issues/detail?id=5107
See here for a workaround to that problem:
Google Maps iOS SDK: How do I get accurate latitude and longitude coordinates from a camera's visibleRegion?

How do I determine if the current user location is inside of my MKCoordinateRegion?

I have a coordinate region that I have determined contains the limits of what I want to show for my app. I have set this up as an MKCoordinateRegion with center point lat, longitude and a span. How do I determine if the current userLocation is inside of my coordinate region?
Use map rects. Here's an example using the map's current visible rect. With regards to your question, you could use convertRegion:toRectToView: to first convert your region to a MKMapRect beforehand.
MKMapPoint userPoint = MKMapPointForCoordinate(mapView.userLocation.location.coordinate);
MKMapRect mapRect = mapView.visibleMapRect;
BOOL inside = MKMapRectContainsPoint(mapRect, userPoint);
Swift 3 version of firstresponder's answer:
let userPoint = MKMapPointForCoordinate(mapView.userLocation.coordinate)
let mapRect = mapView.visibleMapRect
let inside = MKMapRectContainsPoint(mapRect, userPoint)
Pretty much the same. This API has not been Swift-ified (i.e., updated to conform to the Swift API design guidelines) yet. It really should be...
let userPoint = mapView.userLocation.coordinate.mapPoint
let inside = mapView.visibleMapRect.contains(userPoint)
There is a simple solution to decide if a point is inside your area if the area is given by a polygon using the ray casting algorithm: See here http://en.wikipedia.org/wiki/Point_in_polygon
As a starting point use a location guaranteed to be outside your region, e.g. (geographic) north pole.

Coordinates of bottom-left corner of a mapView off by a few pixels

I'm trying to put an Annotation the lower left corner of currently visible map region, although the following code
CLLocationCoordinate2D origin = getOriginOfRegion(mapView.region);
[[[SimpleAnnotation alloc] initWithCoords:origin] autorelease];
...
extern CLLocationCoordinate2D getOriginOfRegion(MKCoordinateRegion region){
return CLLocationCoordinate2DMake(region.center.latitude-region.span.latitudeDelta/2,
region.center.longitude-region.span.longitudeDelta/2);
}
where SimpleAnnotation is as simple as it gets; puts the pin point a few degrees off the actual corner:
On the right side zoom level is higher, and as you can see the mistake is significantly smaller.
I've tried doing some math to counter it, considering that the error may be something related to projecting elliptical coordinates onto a plane, but didn't come up with anything useful.
Has anyone solved this problem? I've noticed that latitude values are not 'linear' on a mapView, I need some hints to understand how to transform them.
I don't know if this will work but I suggest you try letting the map view convert view coordinates to map coordinates with convertPoint:toCoordinateFromView:.
For example:
CGPoint bottomLeftPoint = CGPointMake(CGRectGetMinX(map.bounds), CGRectGetMaxY(map.bounds));
CLLocationCoordinate2D bottomLeftCoord = [map convertPoint:bottomLeftPoint toCoordinateFromView:map];