MKMapView MKCircle呈现一个半径太大的圆圈

时间:2015-03-05 07:22:19

标签: ios swift geolocation mkmapview

我面临着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()
}

结果很奇怪:

Center in New York City

Center in far north from NYC

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

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

修改

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

enter image description here

(小圈jsut代表一个中心)

3 个答案:

答案 0 :(得分:8)

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

正如Anna和Warren都提到的那样,这不是一个错误 - 这是预期的行为。但是,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:

enter image description here enter image description here

这是最终输出的样本,附加MKGeodesicPolyline叠加(测地线表示球面上最短的曲线),显示曲线的实际构建方式:

enter image description here

答案 1 :(得分:6)

回答OP的问题

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

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

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

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

显示MKMapView

的投影功能的示例

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

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

MKMapView_FixedRadius MKMapView_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 

答案 2 :(得分:0)

如果有人想在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
    }

}