Convert span value into meters on a mapview - iphone

Whenever the user zoom in or out the map i need to know how many meters are currently represented on the map (width or height).
What i need is the inverse function of MKCoordinateRegionMakeWithDistance to calculate the distance represented by the current map span.
I tried the following code but i get wrong results :
- (void)mapView:(MKMapView *)mapView regionDidChangeAnimated:(BOOL)animated {
MKMapRect mRect = self.map.visibleMapRect;
MKMapPoint northMapPoint = MKMapPointMake(MKMapRectGetMidX(mRect), MKMapRectGetMinY(mRect));
MKMapPoint southMapPoint = MKMapPointMake(MKMapRectGetMidX(mRect), MKMapRectGetMaxY(mRect));
self.currentDist = MKMetersBetweenMapPoints(northMapPoint, southMapPoint);
}
If i set the map region to 1500 meters i get something like 1800 as a result..
Thanks for your help,
Vincent

Actually it was a really stupid mistake, if i do the same operation along the X axis then i get the correct value :
- (void)mapView:(MKMapView *)mapView regionDidChangeAnimated:(BOOL)animated {
MKMapRect mRect = self.map.visibleMapRect;
MKMapPoint eastMapPoint = MKMapPointMake(MKMapRectGetMinX(mRect), MKMapRectGetMidY(mRect));
MKMapPoint westMapPoint = MKMapPointMake(MKMapRectGetMaxX(mRect), MKMapRectGetMidY(mRect));
self.currentDist = MKMetersBetweenMapPoints(eastMapPoint, westMapPoint);
}

- (void)mapView:(MKMapView *)map regionDidChangeAnimated:(BOOL)animated {
MKCoordinateSpan span = mapView.region.span;
NSLog(#" 1 = ~111 km -> %f = ~ %f km ",span.latitudeDelta,span.latitudeDelta*111);
}
According to the docs http://developer.apple.com/library/ios/#documentation/MapKit/Reference/MapKitDataTypesReference/Reference/reference.html,
latitudeDelta
The amount of north-to-south distance (measured in degrees) to display on the map. Unlike longitudinal distances, which vary based on the latitude, one degree of latitude is always approximately 111 kilometers (69 miles).

Thanks for the post all. I have an app that required a mile radius to figure out how many location records to fetch so this came in handy. Here is the swift equivalent for anyone who might come across this in the future.
let mRect: MKMapRect = self.mapView.visibleMapRect
let eastMapPoint = MKMapPointMake(MKMapRectGetMinX(mRect), MKMapRectGetMidY(mRect))
let westMapPoint = MKMapPointMake(MKMapRectGetMaxX(mRect), MKMapRectGetMidY(mRect))
let currentDistWideInMeters = MKMetersBetweenMapPoints(eastMapPoint, westMapPoint)
let milesWide = currentDistWideInMeters / 1609.34 // number of meters in a mile
println(milesWide)

Swift 4.2
func mapView(_ mapView: MKMapView, regionDidChangeAnimated animated: Bool) {
let mapRect = mapView.visibleMapRect
let westMapPoint = MKMapPoint(x: mapRect.minX, y: mapRect.midY)
let eastMapPoint = MKMapPoint(x: mapRect.maxX, y: mapRect.midY)
let visibleDistance = westMapPoint.distance(to: eastMapPoint)
}

Here's an easier way (to get width and height in meters)...
- (void)mapView:(MKMapView *)mapView regionDidChangeAnimated:(BOOL)animated {
MKMapRect rect = mapView.visibleMapRect;
double mapWidth = MKMapRectGetWidth(rect) / 10;
double mapHeight = MKMapRectGetHeight(rect) / 10;
}

Swift 4
extension MKMapView {
func regionInMeter() -> CLLocationDistance {
let eastMapPoint = MKMapPointMake(MKMapRectGetMinX(visibleMapRect), MKMapRectGetMidY(visibleMapRect))
let westMapPoint = MKMapPointMake(MKMapRectGetMaxX(visibleMapRect), MKMapRectGetMidY(visibleMapRect))
return MKMetersBetweenMapPoints(eastMapPoint, westMapPoint)
}
}

Related

How to calculate the distance travelled by user from current location in swift 3.0

I am using UImapkit & core location frameworks
How will I get the total polyLine distance & travelled time
this my code
func locationManager(_ manager: CLLocationManager!, didUpdateToLocation newLocation: CLLocation!, fromLocation oldLocation: CLLocation!) {
if let oldLocationNew = oldLocation as CLLocation?{
let oldCoordinates = oldLocationNew.coordinate
let newCoordinates = newLocation.coordinate
var area = [oldCoordinates, newCoordinates]
print(area)
let polyline = MKPolyline(coordinates: &area, count: area.count)
mkMapView.add(polyline)
}
//calculation for location selection for pointing annoation
if (previousLocation as CLLocation?) != nil{
if previousLocation.distance(from: newLocation) > 10 {
addAnnotationsOnMap(newLocation)
previousLocation = newLocation
}
}else{
//case if previous location doesn't exists
addAnnotationsOnMap(newLocation)
previousLocation = newLocation
}
}
You can use CLLocation to calculate the distance between two locations.
i.e
let distance = newLocation.distance(from: oldLocation)
Once you calculated the distance, then you can easily calculate the travel time by using the speed distance time formula
speed = distance / time
since you know the distance and if you assume the speed, you can calculate time taken for the travel as
time = distance / speed
Hope this stuff will help you.

MKMapView setRegion animated does not show animation

I have a MKMapView with annotation pins. When the view was loaded the nearest pin gets searched and the map will get zoomed so it shows both, the user's location and the nearest pin. I do that with [map setRegion:region animated:YES];. Everything works fine up to here. The same method is also called by tapping on a button which locates the user and then does exactly what I just described.
I also have a search field with which the user can search for map points. When the user taps on one of the search results the map sets the region so the searched pin is in the middle.
Now, there's something strange with that. I also set this region animated, at least I do the same command as above. But if the map point is too far away from the current visible part of the map it doesn't show the animation when changing the region.
Am I missing something? I've already had a look at Apples docs, they don't mention anything regarding any maximum distance for animations.
I'm looking forward to any help!
Update 1:
Just tested it again in the Simulator. An interesting fact is, that when I search for a MapPoint for the first time and then select a search result it doesn't animate. If I perform another search just after the first one and select a result it does animate. As soon as I tap on the locate button which brings the user back to his location and the closest point it doesn't animate for this setRegion: and the first search after that. But only in the Simulator, on my 4S it does exactly what I've described in the original question above.
Update 2:
In the comments I was asked to provide example coordinates.
So here the coordinates for the first step (searching of the own location and the nearest pin):
My Location: 47.227131 / 8.264251
Nearest pin: 47.251347 / 8.357191
The distance between them is about 22 kilometers. The center of the map is the center between the two pins. The distance from the center to the screen border is 1.5 times the distance between the two points which means about 33 kilometers in this case.
And here a set of coordinates for the second step (searching a map point and selecting it):
Searched pin: 46.790680 / 9.818824
The distance to the screen border is here fixed to 500 meters.
I've tested this issue with a simple demo application on iOS 6 and iOS 7 beta. It turns out that the map view actually not always animates the transition between regions. It depends on how far the regions lay apart. For example a transition from Paris to London is not animated. But if you first zoom out a little bit and then go to London it will be animated.
The documentation says:
animated: Specify YES if you want the map view to animate the
transition to the new region or NO if you want the map to center on
the specified region immediately.
But as we've seen, we can not rely on the animation. We can only tell the map view that the transition should be animated. MapKit decides whether an animation is appropriate. It tells the delegate if the transition will be animated in -(void)mapView:(MKMapView *)mapView regionWillChangeAnimated:(BOOL)animated.
In order to consistently animate the region change in all cases you will need to animate to a intermediate region first. Let A be the current map region and B the target region. If there is an intersection between the regions you can transition directly. (Transform the MKCoordinateRegion to an MKMapRect and use MKMapRectIntersection to find the intersection). If there is no intersection, calculate a region C that spans both regions (use MKMapRectUnion and MKCoordinateRegionForMapRect). Then first go to to region C and in regionDidChangeAnimated go to region B.
Sample code:
MKCoordinateRegion region = _destinationRegion;
MKMapRect rect = MKMapRectForCoordinateRegion(_destinationRegion);
MKMapRect intersection = MKMapRectIntersection(rect, _mapView.visibleMapRect);
if (MKMapRectIsNull(intersection)) {
rect = MKMapRectUnion(rect, _mapView.visibleMapRect);
region = MKCoordinateRegionForMapRect(rect);
_intermediateAnimation = YES;
}
[_mapView setRegion:region animated:YES];
-(void)mapView:(MKMapView *)mapView regionDidChangeAnimated:(BOOL)animated
{
if (_intermediateAnimation) {
_intermediateAnimation = NO;
[_mapView setRegion:_destinationRegion animated:YES];
}
}
This helper method is taken from here
MKMapRect MKMapRectForCoordinateRegion(MKCoordinateRegion 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));
}
The WWDC 2013 session 309 Putting Map Kit in Perspective explains how to do such complex transitions in iOS 7.
Here are the functions by #Felix rewritten to Swift 4:
// MARK: - MapView help properties
var destinationRegion: MKCoordinateRegion?
var intermediateAnimation = false
func center() {
// Center map
var initialCoordinates = CLLocationCoordinate2D(latitude: 49.195061, longitude: 16.606836)
var regionRadius: CLLocationDistance = 5000000
destinationRegion = MKCoordinateRegionMakeWithDistance(initialCoordinates, regionRadius * 2.0, regionRadius * 2.0)
centreMap(on: destinationRegion!)
}
private func mapRect(forCoordinateRegion region: MKCoordinateRegion) -> MKMapRect {
let topLeft = CLLocationCoordinate2D(latitude: region.center.latitude + (region.span.latitudeDelta/2), longitude: region.center.longitude - (region.span.longitudeDelta/2))
let bottomRight = CLLocationCoordinate2D(latitude: region.center.latitude - (region.span.latitudeDelta/2), longitude: region.center.longitude + (region.span.longitudeDelta/2))
let a = MKMapPointForCoordinate(topLeft)
let b = MKMapPointForCoordinate(bottomRight)
return MKMapRect(origin: MKMapPoint(x:min(a.x,b.x), y:min(a.y,b.y)), size: MKMapSize(width: abs(a.x-b.x), height: abs(a.y-b.y)))
}
func centreMap(on region: MKCoordinateRegion) {
var region = region
var rect = mapRect(forCoordinateRegion: region)
let intersection = MKMapRectIntersection(rect, mapView.visibleMapRect)
if MKMapRectIsNull(intersection) {
rect = MKMapRectUnion(rect, mapView.visibleMapRect)
region = MKCoordinateRegionForMapRect(rect)
intermediateAnimation = true
}
mapView.setRegion(region, animated: true)
}
// MARK: - MKMapViewDelegate
func mapView(_ mapView: MKMapView, regionDidChangeAnimated animated: Bool) {
if intermediateAnimation, let region = destinationRegion {
intermediateAnimation = false
mapView.setRegion(region, animated: true)
}
}
I also had this problem where it would not always animate, sometimes it would just jump and dissolve instead. However, I noticed that if you animate the camera instead of the region, it consistently animates.
But using the camera, you have to set the eye distance/altitude instead of the lat/lon span. I have a simple calculation for that below which is rough, it basically just sets the altitude (in meters) to the same number of meters as the longitude span of the region. If you wanted exact accuracy you'd have to figure out the number of meters per degree for the region's latitude, which changes slightly because the earth is not a perfect sphere. Of course you could multiply that value to widen or narrow the view to taste.
Swift 4.1 example code:
/* -------------------------------------------------------------------------- */
func animateToMapRegion(_ region: MKCoordinateRegion) {
// Quick and dirty calculation of altitude necessary to show region.
// 111 kilometers per degree longitude.
let metersPerDegree: Double = 111 * 1_000
let altitude = region.span.longitudeDelta * metersPerDegree
let camera = MKMapCamera(lookingAtCenter: region.center, fromEyeCoordinate: region.center, eyeAltitude: altitude)
self.mapView.setCamera(camera, animated: true)
}
You simply have to get your current location and then call this function:
◙ import MapKit
◙ var appleMapView = MKMapView()
◙ var currentCoordinate: CLLocationCoordinate2D?
currentCoordinate must be your current location coordinates:
◙ if let currentLoc = self.currentCoordinate {
let center = CLLocationCoordinate2D(latitude: currentLoc.latitude, longitude: currentLoc.longitude)
let region = MKCoordinateRegion(center: center, span: MKCoordinateSpan(latitudeDelta: 0.01, longitudeDelta: 0.01))
appleMapView.setRegion(region, animated: true)
}
To anyone who has the same question and is using Swift and is using a tableView:
I called setRegion after dismissing the tableView, and it did not show animation. This is my code before editing:
dismiss(animated: true, completion: nil)
a function that calls setRegion
Then I changed it to:
dismiss(animated: true, completion: {
a function that calls setRegion
})
This time it works.

How to compare two different map-view regions and find difference between them

Hi Guys I am Developing Map-view Base app. I Have almost done everything with map-view but I unable to compare two different map-view regions and find new area of map-view. For example, if the user drags the map, I want to find how much the region has changed.
partial answer. Equatable compliance for MKCoordinateRegion
extension MKCoordinateRegion: Equatable
{
public static func == (lhs: MKCoordinateRegion, rhs: MKCoordinateRegion) -> Bool
{
if lhs.center.latitude != rhs.center.latitude || lhs.center.longitude != rhs.center.longitude
{
return false
}
if lhs.span.latitudeDelta != rhs.span.latitudeDelta || lhs.span.longitudeDelta != rhs.span.longitudeDelta
{
return false
}
return true
}
}
First you need to find the initial map region. Say your map is named mapView...you can first find this by (in your viewDidLoad):
CLLocationCoordinate2D center = mapView.centerCoordinate;
CLLocationDegrees lat = center.latitude;
CLLocationDegrees lon = center.longitude;
MKCoordinateRegion region = mapView.region;
MKCoordinateSpan span = region.span;
//Assuming they have been declared as instance variables of type double
current_lat_low = lat - span.latitudeDelta / 2.0;
current_lat_high = lat + span.latitudeDelta / 2.0;
current_lon_low = lon - span.longitudeDelta / 2.0;
current_lon_high = lon + span.longitudeDelta / 2.0;
This will give you the initial area of the map shown. Then in
- (void)mapView:(MKMapView*)mapView regionDidChangeAnimated:(BOOL)animated
{
CLLocationCoordinate2D center = mapView.centerCoordinate;
CLLocationDegrees lat = center.latitude;
CLLocationDegrees lon = center.longitude;
MKCoordinateRegion region = mapView.region;
MKCoordinateSpan span = region.span;
double lat_low = lat - span.latitudeDelta / 2.0;
double lat_high = lat + span.latitudeDelta / 2.0;
double lon_low = lon - span.longitudeDelta / 2.0;
double lon_high = lon + span.longitudeDelta / 2.0;
//do any work comparing the initial lat/lons with the new values
.....
//set current lat/lon to be the new lat/lon after work is complete
current_lat_low = lat_low;
current_lat_high = lat_high;
current_lon_low = lon_low;
current_lon_high = lon_high;
}
I guess it really depends on what you are trying to do. You could, for example, be trying to compare the difference between the centers of the two regions. However, you mentioned area.
An MKCoordinateRegion is just a rectangle and unless the zoom level changes will just have the same total area (LxW units squared). So I'll point you to a CLCircularRegion whose area can be computed using its radius which together with the center is an exposed property.
There are a number of ways to get the circular region from your map data. Your question does not make clear whether you are interested in the distance between the center of the areas but there should be plenty of SO answers out there on finding that difference.
If one of these scenarios is not exactly what you want to achieve, leave me a comment.
(source)
This answer shows you how to compare two map points. You could use this on the centre of your map regions: link.

how to find current zoom level of MKMapView?

I want to know the current zoom level of MKMapView in iphone programming, how can I do that?
Actually I have one app, that is taking few arguments(Radius from center to corner of MKMapView) are returning the store details in that are, when I am on MKMapView, the radius is very high and it changes when Radius is smaller, so I want to know the zoom level and setup my webservice according to that, How can I get zoom level of current MKMapView visible area?
I created very simple helper subclass for it:
#define MERCATOR_RADIUS 85445659.44705395
#define MAX_GOOGLE_LEVELS 20
#interface MKMapView (ZoomLevel)
- (double)getZoomLevel;
#end
#implementation MKMapView (ZoomLevel)
- (double)getZoomLevel
{
CLLocationDegrees longitudeDelta = self.region.span.longitudeDelta;
CGFloat mapWidthInPixels = self.bounds.size.width;
double zoomScale = longitudeDelta * MERCATOR_RADIUS * M_PI / (180.0 * mapWidthInPixels);
double zoomer = MAX_GOOGLE_LEVELS - log2( zoomScale );
if ( zoomer < 0 ) zoomer = 0;
// zoomer = round(zoomer);
return zoomer;
}
#end
You can use span inside the region property of the MKMapView. It is defined like this:
typedef struct {
CLLocationDegrees latitudeDelta;
CLLocationDegrees longitudeDelta;
} MKCoordinateSpan;
Take a look at the documentation. It is well explained there.
The easiest way to get an Integer of the current zoom level, is by using the MapView function: regionDidChangeAnimated. This function recognizes every change in zoom and will give you the basis for the calculation of the zoom factor.
Just insert this function into your MapView class (works for Swift 3.0):
var mapView: MKMapView! = nil
...
func mapView(_ mapView: MKMapView, regionDidChangeAnimated animated: Bool) {
let zoomWidth = mapView.visibleMapRect.size.width
let zoomFactor = Int(log2(zoomWidth)) - 9
print("...REGION DID CHANGE: ZOOM FACTOR \(zoomFactor)")
}
And you will get a zoomFactor value out of it, where 0 is the most near point you can zoom into the map and every higher value is a far far away zoom... :-)
All the previous answers do not take into consideration current map rotation. MKMapView's longitudeDelta differs for non-rotated map and rotated map.
Here is a great function for straight map zoom calculation: https://stackoverflow.com/a/15020534/4923516
And here is my improvement in Swift, that takes into consideration map rotation and returns current zoom level:
class MyMapView : MKMapView {
func getZoom() -> Double {
// function returns current zoom of the map
var angleCamera = self.camera.heading
if angleCamera > 270 {
angleCamera = 360 - angleCamera
} else if angleCamera > 90 {
angleCamera = fabs(angleCamera - 180)
}
let angleRad = M_PI * angleCamera / 180 // camera heading in radians
let width = Double(self.frame.size.width)
let height = Double(self.frame.size.height)
let heightOffset : Double = 20 // the offset (status bar height) which is taken by MapKit into consideration to calculate visible area height
// calculating Longitude span corresponding to normal (non-rotated) width
let spanStraight = width * self.region.span.longitudeDelta / (width * cos(angleRad) + (height - heightOffset) * sin(angleRad))
return log2(360 * ((width / 256) / spanStraight)) + 1;
}
}
You can download sample project in my repo: https://github.com/d-babych/mapkit-wrap
If it helps you can also use mapView.camera.altitude to get the current altitude of the currently displayed map region.
This is what helped me when I searched how to get the map zoom level.

Is there way to limit MKMapView maximum zoom level?

the question is - is there a way to limit maximum zoom level for MKMapView? Or is there a way to track when user zooms to the level where there's no map image available?
If you're working with iOS 7+ only, there's a new camera.altitude property that you can get/set to enforce a zoom level. Its equivalent to azdev's solution, but no external code is required.
In testing, I also discovered that it was possible to enter an infinite loop if you repeatedly tried to zoom in at detail, so I have a var to prevent that in my code below.
- (void)mapView:(MKMapView *)mapView regionDidChangeAnimated:(BOOL)animated {
// enforce maximum zoom level
if (_mapView.camera.altitude < 120.00 && !_modifyingMap) {
_modifyingMap = YES; // prevents strange infinite loop case
_mapView.camera.altitude = 120.00;
_modifyingMap = NO;
}
}
You could use the mapView:regionWillChangeAnimated: delegate method to listen for region change events, and if the region is wider than your maximum region, set it back to the max region with setRegion:animated: to indicate to your user that they can't zoom out that far. Here's the methods:
- (void)mapView:(MKMapView *)mapView regionWillChangeAnimated:(BOOL)animated
- (void)setRegion:(MKCoordinateRegion)region animated:(BOOL)animated
I just spent some time working on this for an app i'm building. Here's what I came up with:
I started with Troy Brant's script on this page which is a nicer way to set the map view I think.
I added a method to return the current zoom level.
In MKMapView+ZoomLevel.h:
- (double)getZoomLevel;
In MKMapView+ZoomLevel.m:
// Return the current map zoomLevel equivalent, just like above but in reverse
- (double)getZoomLevel{
MKCoordinateRegion reg=self.region; // the current visible region
MKCoordinateSpan span=reg.span; // the deltas
CLLocationCoordinate2D centerCoordinate=reg.center; // the center in degrees
// Get the left and right most lonitudes
CLLocationDegrees leftLongitude=(centerCoordinate.longitude-(span.longitudeDelta/2));
CLLocationDegrees rightLongitude=(centerCoordinate.longitude+(span.longitudeDelta/2));
CGSize mapSizeInPixels = self.bounds.size; // the size of the display window
// Get the left and right side of the screen in fully zoomed-in pixels
double leftPixel=[self longitudeToPixelSpaceX:leftLongitude];
double rightPixel=[self longitudeToPixelSpaceX:rightLongitude];
// The span of the screen width in fully zoomed-in pixels
double pixelDelta=abs(rightPixel-leftPixel);
// The ratio of the pixels to what we're actually showing
double zoomScale= mapSizeInPixels.width /pixelDelta;
// Inverse exponent
double zoomExponent=log2(zoomScale);
// Adjust our scale
double zoomLevel=zoomExponent+20;
return zoomLevel;
}
This method relies on a few private methods in the code linked above.
I added this in to my MKMapView delegate (as #vladimir recommended above)
- (void)mapView:(MKMapView *)mapView regionDidChangeAnimated:(BOOL)animated {
NSLog(#"%f",[mapView getZoomLevel]);
if([mapView getZoomLevel]<10) {
[mapView setCenterCoordinate:[mapView centerCoordinate] zoomLevel:10 animated:TRUE];
}
}
This has the effect of re-zooming if the user gets too far out. You can use regionWillChangeAnimated to prevent the map from 'bouncing' back in.
Regarding the looping comments above, it looks like this method only iterates once.
Yes, this is doable. First, extend MKMapView by using MKMapView+ZoomLevel.
Then, implement this in your MKMapViewDelegate:
- (void)mapView:(MKMapView *)mapView regionDidChangeAnimated:(BOOL)animated
{
// Constrain zoom level to 8.
if( [mapView zoomLevel] < 8 )
{
[mapView setCenterCoordinate:mapView.centerCoordinate
zoomLevel:8
animated:NO];
}
}
Here is code rewritten in Swift 3 using MKMapView+ZoomLevel and #T.Markle answer:
import Foundation
import MapKit
fileprivate let MERCATOR_OFFSET: Double = 268435456
fileprivate let MERCATOR_RADIUS: Double = 85445659.44705395
extension MKMapView {
func getZoomLevel() -> Double {
let reg = self.region
let span = reg.span
let centerCoordinate = reg.center
// Get the left and right most lonitudes
let leftLongitude = centerCoordinate.longitude - (span.longitudeDelta / 2)
let rightLongitude = centerCoordinate.longitude + (span.longitudeDelta / 2)
let mapSizeInPixels = self.bounds.size
// Get the left and right side of the screen in fully zoomed-in pixels
let leftPixel = self.longitudeToPixelSpaceX(longitude: leftLongitude)
let rightPixel = self.longitudeToPixelSpaceX(longitude: rightLongitude)
let pixelDelta = abs(rightPixel - leftPixel)
let zoomScale = Double(mapSizeInPixels.width) / pixelDelta
let zoomExponent = log2(zoomScale)
let zoomLevel = zoomExponent + 20
return zoomLevel
}
func setCenter(coordinate: CLLocationCoordinate2D, zoomLevel: Int, animated: Bool) {
let zoom = min(zoomLevel, 28)
let span = self.coordinateSpan(centerCoordinate: coordinate, zoomLevel: zoom)
let region = MKCoordinateRegion(center: coordinate, span: span)
self.setRegion(region, animated: true)
}
// MARK: - Private func
private func coordinateSpan(centerCoordinate: CLLocationCoordinate2D, zoomLevel: Int) -> MKCoordinateSpan {
// Convert center coordiate to pixel space
let centerPixelX = self.longitudeToPixelSpaceX(longitude: centerCoordinate.longitude)
let centerPixelY = self.latitudeToPixelSpaceY(latitude: centerCoordinate.latitude)
// Determine the scale value from the zoom level
let zoomExponent = 20 - zoomLevel
let zoomScale = NSDecimalNumber(decimal: pow(2, zoomExponent)).doubleValue
// Scale the map’s size in pixel space
let mapSizeInPixels = self.bounds.size
let scaledMapWidth = Double(mapSizeInPixels.width) * zoomScale
let scaledMapHeight = Double(mapSizeInPixels.height) * zoomScale
// Figure out the position of the top-left pixel
let topLeftPixelX = centerPixelX - (scaledMapWidth / 2)
let topLeftPixelY = centerPixelY - (scaledMapHeight / 2)
// Find delta between left and right longitudes
let minLng: CLLocationDegrees = self.pixelSpaceXToLongitude(pixelX: topLeftPixelX)
let maxLng: CLLocationDegrees = self.pixelSpaceXToLongitude(pixelX: topLeftPixelX + scaledMapWidth)
let longitudeDelta: CLLocationDegrees = maxLng - minLng
// Find delta between top and bottom latitudes
let minLat: CLLocationDegrees = self.pixelSpaceYToLatitude(pixelY: topLeftPixelY)
let maxLat: CLLocationDegrees = self.pixelSpaceYToLatitude(pixelY: topLeftPixelY + scaledMapHeight)
let latitudeDelta: CLLocationDegrees = -1 * (maxLat - minLat)
return MKCoordinateSpan(latitudeDelta: latitudeDelta, longitudeDelta: longitudeDelta)
}
private func longitudeToPixelSpaceX(longitude: Double) -> Double {
return round(MERCATOR_OFFSET + MERCATOR_RADIUS * longitude * M_PI / 180.0)
}
private func latitudeToPixelSpaceY(latitude: Double) -> Double {
if latitude == 90.0 {
return 0
} else if latitude == -90.0 {
return MERCATOR_OFFSET * 2
} else {
return round(MERCATOR_OFFSET - MERCATOR_RADIUS * Double(logf((1 + sinf(Float(latitude * M_PI) / 180.0)) / (1 - sinf(Float(latitude * M_PI) / 180.0))) / 2.0))
}
}
private func pixelSpaceXToLongitude(pixelX: Double) -> Double {
return ((round(pixelX) - MERCATOR_OFFSET) / MERCATOR_RADIUS) * 180.0 / M_PI
}
private func pixelSpaceYToLatitude(pixelY: Double) -> Double {
return (M_PI / 2.0 - 2.0 * atan(exp((round(pixelY) - MERCATOR_OFFSET) / MERCATOR_RADIUS))) * 180.0 / M_PI
}
}
Example of use in your view controller:
func mapView(_ mapView: MKMapView, regionDidChangeAnimated animated: Bool) {
print("Zoom: \(mapView.getZoomLevel())")
if mapView.getZoomLevel() > 6 {
mapView.setCenter(coordinate: mapView.centerCoordinate, zoomLevel: 6, animated: true)
}
}
Use this example to lock the maximum zoom range, also equally you can limit the minimum
map.cameraZoomRange = MKMapView.CameraZoomRange(maxCenterCoordinateDistance: 1200000)
If you are targeting iOS 13+, use the MKMapView setCameraZoomRange method. Simply provide the min and max center coordinate distances (measured in meters).
See Apple's Documentation here: https://developer.apple.com/documentation/mapkit/mkmapview/3114302-setcamerazoomrange
Don't use regionWillChangeAnimated. Use regionDidChangeAnimated
we can also use setRegion(region, animated: true). Normally it will freeze MKMapView if we use regionWillChangeAnimated, but with regionDidChangeAnimated it works perfectly
func mapView(_ mapView: MKMapView, regionDidChangeAnimated animated: Bool) {
mapView.checkSpan()
}
extension MKMapView {
func zoom() {
let region = MKCoordinateRegionMakeWithDistance(userLocation.coordinate, 2000, 2000)
setRegion(region, animated: true)
}
func checkSpan() {
let rect = visibleMapRect
let westMapPoint = MKMapPointMake(MKMapRectGetMinX(rect), MKMapRectGetMidY(rect))
let eastMapPoint = MKMapPointMake(MKMapRectGetMaxX(rect), MKMapRectGetMidY(rect))
let distanceInMeter = MKMetersBetweenMapPoints(westMapPoint, eastMapPoint)
if distanceInMeter > 2100 {
zoom()
}
}
}
The MKMapView has, inside of it, a MKScrollView (private API), that is a subclass of UIScrollView. The delegate of this MKScrollView is its own mapView.
So, in order to control the max zoom do the following:
Create a subclass of MKMapView:
MapView.h
#import <UIKit/UIKit.h>
#import <MapKit/MapKit.h>
#interface MapView : MKMapView <UIScrollViewDelegate>
#end
MapView.m
#import "MapView.h"
#implementation MapView
-(void)scrollViewDidZoom:(UIScrollView *)scrollView {
UIScrollView * scroll = [[[[self subviews] objectAtIndex:0] subviews] objectAtIndex:0];
if (scroll.zoomScale > 0.09) {
[scroll setZoomScale:0.09 animated:NO];
}
}
#end
Then, access the scroll subview and see the zoomScale property. When the zoom is greater than a number, set your max zoom.
The post by Raphael Petegrosso with the extended MKMapView works great with some small modifications.
The version below is also much more "user friendly", as it gracefully "snaps" back to the defined zoom level as soon as the user lets go of the screen, being similar in feel to Apple's own bouncy scrolling.
Edit: This solution is not optimal and will break/damage the map view, I found a much better solution here: How to detect any tap inside an MKMapView. This allows you to intercept pinching and other motions.
MyMapView.h
#import <MapKit/MapKit.h>
#interface MyMapView : MKMapView <UIScrollViewDelegate>
#end
MyMapView.m
#import "MyMapView.h"
#implementation MyMapView
- (void)scrollViewDidEndZooming:(UIScrollView *)scrollView withView:(UIView *)view atScale:(float)scale
{
if (scale > 0.001)
{
[scrollView setZoomScale:0.001 animated:YES];
}
}
#end
For a hard limit, use this:
#import "MyMapView.h"
#implementation MyMapView
-(void)scrollViewDidZoom:(UIScrollView *)scrollView
{
if (scrollView.zoomScale > 0.001)
{
[scrollView setZoomScale:0.001 animated:NO];
}
}
#end
The following code worked for me and is conceptually easy to use because it sets the region based on a distance in meters.
The code is derived from the answer posted by: #nevan-king and the comment posted by #Awais-Fayyaz to use regionDidChangeAnimated
Add the following extension to your MapViewDelegate
var currentLocation: CLLocationCoordinate2D?
extension MyMapViewController: MKMapViewDelegate {
func mapView(_ mapView: MKMapView, regionDidChangeAnimated animated: Bool) {
if self.currentLocation != nil, mapView.region.longitudinalMeters > 1000 {
let initialLocation = CLLocation(latitude: (self.currentLocation?.latitude)!,
longitude: (self.currentLocation?.longitude)!)
let coordinateRegion = MKCoordinateRegionMakeWithDistance(initialLocation.coordinate,
regionRadius, regionRadius)
mapView.setRegion(coordinateRegion, animated: true)
}
}
}
Then define an extension for MKCoordinateRegion as follows.
extension MKCoordinateRegion {
/// middle of the south edge
var south: CLLocation {
return CLLocation(latitude: center.latitude - span.latitudeDelta / 2, longitude: center.longitude)
}
/// middle of the north edge
var north: CLLocation {
return CLLocation(latitude: center.latitude + span.latitudeDelta / 2, longitude: center.longitude)
}
/// middle of the east edge
var east: CLLocation {
return CLLocation(latitude: center.latitude, longitude: center.longitude + span.longitudeDelta / 2)
}
/// middle of the west edge
var west: CLLocation {
return CLLocation(latitude: center.latitude, longitude: center.longitude - span.longitudeDelta / 2)
}
/// distance between south and north in meters. Reverse function for MKCoordinateRegionMakeWithDistance
var latitudinalMeters: CLLocationDistance {
return south.distance(from: north)
}
/// distance between east and west in meters. Reverse function for MKCoordinateRegionMakeWithDistance
var longitudinalMeters: CLLocationDistance {
return east.distance(from: west)
}
}
The above snippet for MKCoordinateRegion was posted by #Gerd-Castan on this question:
Reverse function of MKCoordinateRegionMakeWithDistance?
I've run into this very issue at work and have created something that works fairly well without setting a global limit.
The MapView delegates that I leverage are:
- mapViewDidFinishRendering
- mapViewRegionDidChange
The premise behind my solution is that since a satellite view renders an area with no data it is always the same thing. This dreaded image (http://imgur.com/cm4ou5g) If we can comfortably rely on that fail case we can use it as a key for determining wha the user is seeing. After the map renders, I take a screenshot of the rendered map bounds and determing an average RGB value. Based off of that RGB value, I assume that the area in question has no data. If that's the case I pop the map back out to the last span that was rendered correctly.
The only global check I have is when it starts to check the map, you can increase or decrease that setting based on your needs. Below is the raw code that will accomplish this and will be putting together a sample project for contribution. Any optimizations you can offer would be appreciated and hope it helps.
#property (assign, nonatomic) BOOL isMaxed;
#property (assign, nonatomic) MKCoordinateSpan lastDelta;
self.lastDelta = MKCoordinateSpanMake(0.006, 0.006);
- (void)mapView:(MKMapView *)mapView regionDidChangeAnimated:(BOOL)animated {
if (mapView.mapType != MKMapTypeStandard && self.isMaxed) {
[self checkRegionWithDelta:self.lastDelta.longitudeDelta];
}
}
- (void)checkRegionWithDelta:(float)delta {
if (self.mapView.region.span.longitudeDelta < delta) {
MKCoordinateRegion region = self.mapView.region;
region.span = self.lastDelta;
[self.mapView setRegion:region animated:NO];
} else if (self.mapView.region.span.longitudeDelta > delta) {
self.isMaxed = NO;
}
}
- (void)mapViewDidFinishRenderingMap:(MKMapView *)mapView fullyRendered:(BOOL)fullyRendered {
if (mapView.mapType != MKMapTypeStandard && !self.isMaxed) {
[self checkToProcess:self.lastDelta.longitudeDelta];
}
}
- (void)checkToProcess:(float)delta {
if (self.mapView.region.span.longitudeDelta < delta) {
UIGraphicsBeginImageContext(self.mapView.bounds.size);
[self.mapView.layer renderInContext:UIGraphicsGetCurrentContext()];
UIImage *mapImage = UIGraphicsGetImageFromCurrentImageContext();
[self processImage:mapImage];
}
}
- (void)processImage:(UIImage *)image {
self.mapColor = [self averageColor:image];
const CGFloat* colors = CGColorGetComponents( self.mapColor.CGColor );
[self handleColorCorrection:colors[0]];
}
- (void)handleColorCorrection:(float)redColor {
if (redColor < 0.29) {
self.isMaxed = YES;
[self.mapView setRegion:MKCoordinateRegionMake(self.mapView.centerCoordinate, self.lastDelta) animated:YES];
} else {
self.lastDelta = self.mapView.region.span;
}
}
- (UIColor *)averageColor:(UIImage *)image {
CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
unsigned char rgba[4];
CGContextRef context = CGBitmapContextCreate(rgba, 1, 1, 8, 4, colorSpace, kCGImageAlphaPremultipliedLast | kCGBitmapByteOrder32Big);
CGContextDrawImage(context, CGRectMake(0, 0, 1, 1), image.CGImage);
CGColorSpaceRelease(colorSpace);
CGContextRelease(context);
if(rgba[3] > 0) {
CGFloat alpha = ((CGFloat)rgba[3])/255.0;
CGFloat multiplier = alpha/255.0;
return [UIColor colorWithRed:((CGFloat)rgba[0])*multiplier
green:((CGFloat)rgba[1])*multiplier
blue:((CGFloat)rgba[2])*multiplier
alpha:alpha];
}
else {
return [UIColor colorWithRed:((CGFloat)rgba[0])/255.0
green:((CGFloat)rgba[1])/255.0
blue:((CGFloat)rgba[2])/255.0
alpha:((CGFloat)rgba[3])/255.0];
}
}