MKMapView MKCircle渲染半径太大的圆

我面临着MKCircle外表的奇怪行为。 基本上我试图用一个任意中心绘制一个半径为8500 km的圆。 这是我的代码:

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

我还有一个自定义双击手势处理程序,它会覆盖地图视图的标准处理程序,并允许通过双击地图视图来更改地图中心:

 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() } 

结果很奇怪:

纽约市中心

中心位于纽约市的最北端

您可能会注意到这两个半径之间存在显着差异:第二个半径比第一个半径大!

发生了什么以及如何让它们正确显示?

编辑

感谢@blacksquare,我可以更接近解决方案,但仍然存在北极问题:

在此处输入图像描述

(小圈jsut代表一个中心)

根据Apple的MKCircle文档:“当纬度值从赤道移向极点时,地图点之间的物理距离变小。这意味着需要更多的地图点来表示相同的距离。因此,边界当圆圈的中心点远离赤道并朝向极点移动时,圆形覆盖的矩形变大。“

正如安娜和沃伦都提到的那样,这不是一个错误 – 这是预期的行为。 但是,在boundingMapRectradius之间的文档中似乎存在差异。 文档建议半径是距中心点的度量单位,在您的示例中显然不是这种情况。

我想这里发生的事情是Apple可能从未打算让MKCircle用于你正在使用它的规模上。 MKCircle创建一个2D圆,它既不能是圆形,也不能是投影图上圆形区域的精确表示。

现在,如果您想要做的只是创建一个没有扭曲并且相对于赤道长度的半径的均匀圆,您可以将赤道上的圆的长度设置为基本半径,然后计算比例当前点的半径如下:

 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) } 

但如果你的计划要准确覆盖点击x米范围内的所有空间,那就有点棘手了。 首先,您将获取双击动作中的单击坐标,然后将其用作多边形的中心。

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

addPolygon ,获取坐标并设置叠加层:

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

给定一个点,一个方位和一个角距离(坐标之间的距离除以地球的半径),您可以使用以下公式计算另一个坐标的位置。 务必导入Darwin以便您可以访问三角函数库

 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 } 

getCoordinates我们将度数转换为弧度,然后在我们的半径大于到北极或南极的距离的情况下添加一些锚定坐标。

以下是极点附近曲线的几个例子,半径分别为8500km和850km:

在此处输入图像描述在此处输入图像描述

下面是最终输出的示例,其中包含一个额外的MKGeodesicPolyline叠加(Geodesics表示球面上最短的曲线),它显示了曲线的实际构建方式:

在此处输入图像描述

回答OP的问题

发生的事情是,由于使用了地图投影,显示的地图会出现差异。 MKCircle方法将在任何给定纬度产生精确的圆(假设半径不覆盖到许多纬度),但由于地图投影,它们的大小会有所不同。

为了在较大的缩放级别获得相似的圆圈,您必须相对于纬度更改半径,这可能会影响经度距离。 此外,圈子现在代表什么?

要获得相等的圆,一种方法是使用MapPoints ,它具有纬度相对方法,即MKMetersPerMapPointAtLatitude 。 如果您将此与世界各地的给定数字相乘,则圆圈的大小将相同,但如前所述:此圆圈代表什么?

换句话说: 你需要考虑圆圈应该代表什么,因为在没有修正的情况下使用它确实代表了距离位置的距离,但是由于地图在世界地图上查看多个圆圈时看起来确实不像投影问题!

显示MKMapView投影function的示例

我做了一个小样本应用程序,我在世界各地的不同位置添加了一些圆形的硬编码位置和半径。 这产生了左侧的图像,其具有不同的圆形尺寸。

使用下面代码中描述的纬度相对方法,圆圈具有相同的大小。 我还在巴拿马城增加了一个圆,其半径等于到墨西哥城的距离,表明给定纬度的CLLocationDistance(以米为单位)有些正确。

MKMapView_FixedRadiusMKMapView_LatitudeRelativeRadius

图像生成代码

下面列出了用于将图像生成到右图像的代码的有趣部分。 左图像基于相同的代码,删除了* MKMetersPerMapPointAtLatitude(...)部分和不同的半径。

 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)) 

我在最后添加了一些关于不同距离,地图点等的println ,这些产生了以下输出:

 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 

如果有人想在Swift 3中实现这个,我创建了一个MKPolygon的子类,根据Kellan的优秀答案呈现一个测地圈。

只需创建它

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

这是Swift文件

 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 } }