MKMapView MKCircle displays a circle with a radius too large

I came across the strange behavior of MKCircle. Basically I am trying to draw a circle with a radius of 8500 km with an arbitrary center. Here is my code:

private func addCircle() { mapView.removeOverlays(mapView.overlays) let circle = MKCircle(centerCoordinate: mapCenter, radius: 8500000.0) mapView.addOverlay(circle) } 

I also have a custom double-click gesture descriptor that overwrites the standard map view and allows you to change the center of the map by double-clicking on the map view:

 private func configureGestureRecognizer() { doubleTapGestureRecognizer.addTarget(self, action: Selector("handleDoubleTap:")) doubleTapGestureRecognizer.numberOfTapsRequired = 2 if let subview = mapView.subviews.first as? UIView { subview.addGestureRecognizer(doubleTapGestureRecognizer) } else { println("Can't add a gesture recognizer") } } @objc private func handleDoubleTap(sender: UITapGestureRecognizer) { let point = sender.locationInView(mapView) let location = mapView.convertPoint(point, toCoordinateFromView: mapView) mapCenter = location addCircles() } 

The results are very strange:

Center in new york city

Center in far north from NYC

You can notice a significant difference between these two radii: the second is larger than the first!

What is happening and how can I get them to display correctly?

EDIT

Thanks to @blacksquare, I could get closer to the solution, but there is still a problem with the north pole:

enter image description here

(The small circle jsut represents the center)

+6
source share
3 answers

According to Apple MKCircle documentation: “As latitudes move away from the equator and to the poles, the physical distance between map points is reduced. This means that more map points are required to display the same distance. As a result, the bounding box of the circle overlay becomes larger because the center point this circle departs from the equator and to the poles. "

Since Anna and Warren mentioned this, this is not a mistake - this is the intended behavior. However, there seems to be a discrepancy in documentation between boundingMapRect and radius . The documentation assumes that the radius is a measure in meters from the center point, which clearly does not match your example.

I think what is happening here is that Apple probably never imagined MKCircle to be used at the scale at which you use it. MKCircle creates a 2D circle that cannot be either a circle or an accurate representation of a circular region on a projection map.

Now, if all you want to do is create a homogeneous circle that is not distorted and has a radius relative to its length at the equator, you can set the length of the circle at the equator as the base radius, and then calculate the proportion of the radius at the current point, for example:

 let baseCoord = CLLocationCoordinate2D(latitude: 0, longitude: 0) let radius: Double = 850000.0 override func viewDidLoad() { super.viewDidLoad() mapView.region = MKCoordinateRegion( center: baseCoord, span: MKCoordinateSpan( latitudeDelta: 90, longitudeDelta: 180 ) ) mapCenter = baseCoord let circle = MKCircle(centerCoordinate: mapCenter, radius: radius) baseRadius = circle.boundingMapRect.size.height / 2 mapView.delegate = self configureGestureRecognizer() } private func addCircle() { mapView.removeOverlays(mapView.overlays) let circle = MKCircle(centerCoordinate: mapCenter, radius: radius) var currentRadius = circle.boundingMapRect.size.height / 2 let factor = baseRadius / currentRadius var updatedRadius = factor * radius let circleToDraw = MKCircle(centerCoordinate: mapCenter, radius: updatedRadius) mapView.addOverlay(circleToDraw) } 

But if your plan needs to accurately cover the entire space within x meters of click, it's a little more complicated. First you take the click coordinate in a double click, and then use it as the center of the polygon.

 @objc private func handleDoubleTap(sender: UITapGestureRecognizer) { let point = sender.locationInView(mapView) currentCoord = mapView.convertPoint(point, toCoordinateFromView: mapView) mapCenter = currentCoord addPolygon() } 

In addPolygon get the coordinates and set up your overlays:

 private func addPolygon() { var mapCoords = getCoordinates() mapView.removeOverlays(mapView.overlays) let polygon = MKPolygon(coordinates: &mapCoords, count: mapCoords.count) mapView.addOverlay(polygon) } 

Given the point, bearing, and angular distance (the distance between the coordinates, divided by the radius of the earth), you can calculate the location of another coordinate using the following formula. Be sure to import Darwin so that you can access the trigonometric function library

 let globalRadius: Double = 6371000 let π = M_PI private func getCoordinates() -> [CLLocationCoordinate2D] { var coordinates = [CLLocationCoordinate2D]() let lat1: Double = (currentCoord!.latitude) let long1: Double = (currentCoord!.longitude) + 180 let factor = 30 if let a = annotation { mapView.removeAnnotation(annotation) } annotation = MKPointAnnotation() annotation!.setCoordinate(currentCoord!) annotation!.title = String(format: "%1.2f°, %1.2f°", lat1, long1) mapView.addAnnotation(annotation) var φ1: Double = lat1 * (π / 180) var λ1: Double = long1 * (π / 180) var angularDistance = radius / globalRadius var metersToNorthPole: Double = 0 var metersToSouthPole: Double = 0 for i in Int(lat1)..<89 { metersToNorthPole = metersToNorthPole + 111132.92 - (559.82 * cos(2 * φ1)) + (1.175 * cos(4 * φ1)) } for var i = lat1; i > -89; --i { metersToSouthPole = metersToSouthPole + 111132.92 - (559.82 * cos(2 * φ1)) + (1.175 * cos(4 * φ1)) } var startingBearing = -180 var endingBearing = 180 if metersToNorthPole - radius <= 0 { endingBearing = 0 startingBearing = -360 } for var i = startingBearing; i <= endingBearing; i += factor { var bearing = Double(i) var bearingInRadians: Double = bearing * (π / 180) var φ2: Double = asin(sin(φ1) * cos(angularDistance) + cos(φ1) * sin(angularDistance) * cos(bearingInRadians) ) var λ2 = atan2( sin(bearingInRadians) * sin(angularDistance) * cos(φ1), cos(angularDistance) - sin(φ1) * sin(φ2) ) + λ1 var lat2 = φ2 * (180 / π) var long2 = ( ((λ2 % (2 * π)) - π)) * (180.0 / π) if long2 < -180 { long2 = 180 + (long2 % 180) } if i == startingBearing && metersToNorthPole - radius <= 0 { coordinates.append(CLLocationCoordinate2D(latitude: 90, longitude: long2)) } else if i == startingBearing && metersToSouthPole - radius <= 0 { coordinates.append(CLLocationCoordinate2D(latitude: -90, longitude: long2)) } coordinates.append(CLLocationCoordinate2D(latitude: lat2, longitude: long2)) } if metersToNorthPole - radius <= 0 { coordinates.append(CLLocationCoordinate2D(latitude: 90, longitude: coordinates[coordinates.count - 1].longitude)) } else if metersToSouthPole - radius <= 0 { coordinates.append(CLLocationCoordinate2D(latitude: -90, longitude: coordinates[coordinates.count - 1].longitude)) } return coordinates } 

In getCoordinates we translate degrees into radians, and then add a few more fixing coordinates if our radius is greater than the distance to the north or south poles.

Here are some examples of curves near the pole with radii of 8500 km and 850 km, respectively:

enter image description hereenter image description here

Here is a sample of the final output with an additional overlay of MKGeodesicPolyline (geodesics are the shortest possible curve over a spherical surface), which shows how the curve is constructed:

enter image description here

+8
source

The answer to the OP question

What happens is that the map shown will get discrepancies due to the map projection used. The MKCircle method will create exact circles at any given latitude (provided that the radius is not covered by many latitudes), but they will vary in size due to the map projection.

To get similar circles with a large zoom level, you must change the radius relative to your latitude, and this can affect longitude. Also, what is a circle?

To get equal circles, you need to use MapPoints , which has a relative latitude method, namely MKMetersPerMapPointAtLatitude . If you multiply this number with a given number in different parts of the world, the circles will be the same size, but, as already said: what does this circle represent?

In other words: you need to think about what the circle should represent, because it is used without correction, it represents the distance from the location, but it does not seem to look like several circles on the world map due to problems with the map projection!

MKMapView projection display example

I made a small sample application in which I added several circles in different places around the world with hard-coded location and radius. This created an image on the left that has different circle sizes.

Using the relative latitude method, as shown below in the code, the circles are the same size. I also added one circle in Panama City with a radius equal to the distance to Mexico City, which indicates that the CLLocationDistance (in meters) at a given latitude is somewhat correct.

MKMapView_FixedRadiusMKMapView_LatitudeRelativeRadius

Image Generation Code

An interesting part of the code used to create the image for the correct image is given below. The left image was based on the same code with the removal of the * MKMetersPerMapPointAtLatitude(...) and a different radius.

 let panamaCityLoc = CLLocationCoordinate2D(latitude: 8.9936000, longitude: -79.51979300) let mexicoCityLoc = CLLocationCoordinate2D(latitude: 19.4284700, longitude: -99.1276600) let newYorkLoc = CLLocationCoordinate2D(latitude: 40.7142700, longitude: -74.0059700) let nuukLoc = CLLocationCoordinate2D(latitude: 64.1834700, longitude: -51.7215700) let northlyLoc = CLLocationCoordinate2D(latitude: 80.0000000, longitude: -68.00) var mapCenter = nuukLoc mapView.centerCoordinate = mapCenter var radiusInSomething : CLLocationDistance = 10000000.0 mapView.removeOverlays(mapView.overlays) mapView.addOverlay(MKCircle(centerCoordinate: nuukLoc, radius: radiusInSomething * MKMetersPerMapPointAtLatitude(nuukLoc.latitude))) mapView.addOverlay(MKCircle(centerCoordinate: panamaCityLoc, radius: radiusInSomething * MKMetersPerMapPointAtLatitude(panamaCityLoc.latitude))) mapView.addOverlay(MKCircle(centerCoordinate: newYorkLoc, radius: radiusInSomething * MKMetersPerMapPointAtLatitude(newYorkLoc.latitude))) mapView.addOverlay(MKCircle(centerCoordinate: mexicoCityLoc, radius: radiusInSomething * MKMetersPerMapPointAtLatitude(mexicoCityLoc.latitude))) mapView.addOverlay(MKCircle(centerCoordinate: northlyLoc, radius: radiusInSomething * MKMetersPerMapPointAtLatitude(northlyLoc.latitude))) // Try to figure out something related to distances... var panamaCityMapPoint = MKMapPointForCoordinate(panamaCityLoc) var mexicoCityMapPoint = MKMapPointForCoordinate(mexicoCityLoc) var distancePanamaToMexixo = MKMetersBetweenMapPoints(panamaCityMapPoint, mexicoCityMapPoint) println("Distance Panama City to Mexico City according to dateandtime.info: 2410 km") println("Distance Panama City to Mexico: \(distancePanamaToMexixo) CLLocationDistance (or m)") println(" meters/MapPoint at latitude Panama City: \( MKMetersPerMapPointAtLatitude(panamaCityLoc.latitude) ) ") println(" in mapPoints: \( distancePanamaToMexixo / MKMetersPerMapPointAtLatitude(panamaCityLoc.latitude) ) ") mapView.addOverlay(MKCircle(centerCoordinate: panamaCityLoc, radius: distancePanamaToMexixo)) 

I added some println regarding different distances, map points, etc. at the end, and they reached the following conclusion:

 Distance Panama City to Mexico City according to dateandtime.info: 2410 km Distance Panama City to Mexico: 2408968.73912751 CLLocationDistance (or m) meters/MapPoint at latitude Panama City: 0.146502523951599 in mapPoints: 16443189.333198 
+6
source

If someone wants to implement this in Swift 3, I created a subclass of MKPolygon that displays a survey circle based on Kellan's excellent answer.

Just create it with

 let circle = MKGeodesicCircle(center: CLLocationCoordinate2D, radius: 100000) 

This is a Swift file

 import UIKit import Darwin import CoreLocation import MapKit class MKGeodesicCircle: MKPolygon { convenience init(center: CLLocationCoordinate2D, radius: CLLocationDistance) { self.init(center: center, radius: radius, fromRadial: 0, toRadial: 360) } convenience init(center: CLLocationCoordinate2D, radius: CLLocationDistance, fromRadial: CLLocationDegrees, toRadial:CLLocationDegrees) { let currentCoord:CLLocationCoordinate2D! currentCoord = center let coords = MKGeodesicCircle.getCoordinates(currentCoord: currentCoord, radius: radius, fromRadial: fromRadial, toRadial: toRadial) self.init() self.init(coordinates: coords, count: coords.count) } class func getCoordinates(currentCoord: CLLocationCoordinate2D!, radius: CLLocationDistance, fromRadial: CLLocationDegrees, toRadial: CLLocationDegrees) -> [CLLocationCoordinate2D] { let globalRadius: Double = 6371000 let π = M_PI var coordinates = [CLLocationCoordinate2D]() let lat1: Double = (currentCoord!.latitude) let long1: Double = (currentCoord!.longitude) + 180 let factor = 3 let φ1: Double = lat1 * (π / 180) let λ1: Double = long1 * (π / 180) let angularDistance = radius / globalRadius var metersToNorthPole: Double = 0 var metersToSouthPole: Double = 0 for _ in Int(lat1)..<89 { metersToNorthPole = metersToNorthPole + 111132.92 - (559.82 * cos(2 * φ1)) + (1.175 * cos(4 * φ1)) } for _ in stride(from: lat1, through: -89, by: -1) { metersToSouthPole = metersToSouthPole + 111132.92 - (559.82 * cos(2 * φ1)) + (1.175 * cos(4 * φ1)) } var startingBearing = Int(fromRadial) - 180 var endingBearing = Int(toRadial) - 180 if metersToNorthPole - radius <= 0 { endingBearing = Int(fromRadial) - 0 startingBearing = Int(toRadial) * -1 } for i in stride(from: startingBearing, through: endingBearing, by: factor) { //for var i = startingBearing; i <= endingBearing; i += factor { let bearing = Double(i) let bearingInRadians: Double = bearing * (π / 180) let φ2: Double = asin(sin(φ1) * cos(angularDistance) + cos(φ1) * sin(angularDistance) * cos(bearingInRadians) ) let λ2 = atan2( sin(bearingInRadians) * sin(angularDistance) * cos(φ1), cos(angularDistance) - sin(φ1) * sin(φ2) ) + λ1 let lat2 = φ2 * (180 / π) var long2 = ( ((λ2.truncatingRemainder(dividingBy: (2 * π)) ) - π)) * (180.0 / π) if long2 < -180 { long2 = 180 + (long2.truncatingRemainder(dividingBy: 180)) } if i == startingBearing && metersToNorthPole - radius <= 0 { coordinates.append(CLLocationCoordinate2D(latitude: 90, longitude: long2)) } else if i == startingBearing && metersToSouthPole - radius <= 0 { coordinates.append(CLLocationCoordinate2D(latitude: -90, longitude: long2)) } coordinates.append(CLLocationCoordinate2D(latitude: lat2, longitude: long2)) } if metersToNorthPole - radius <= 0 { coordinates.append(CLLocationCoordinate2D(latitude: 90, longitude: coordinates[coordinates.count - 1].longitude)) } else if metersToSouthPole - radius <= 0 { coordinates.append(CLLocationCoordinate2D(latitude: -90, longitude: coordinates[coordinates.count - 1].longitude)) } return coordinates } } 
0
source

Source: https://habr.com/ru/post/983356/


All Articles