将CLLocation移动x米

32

我有一个定义好的CLLocation对象,想要将该点向东移动x米并向南移动y米,如何实现?


2
请记住,您并非总是可以从地球上所有点向北/向南移动 - 例如,如果您在北极,您只能向南走。您在这里想做什么? - user149341
@duskwuff:简单来说,我希望我的userPos向右走100步,然后再向左走100步 :-) - Oliver
11个回答

43
一个转换为Swift的代码,来自于这个回答

func locationWithBearing(bearingRadians:Double, distanceMeters:Double, origin:CLLocationCoordinate2D) -> CLLocationCoordinate2D {
    let distRadians = distanceMeters / (6372797.6) // earth radius in meters

    let lat1 = origin.latitude * M_PI / 180
    let lon1 = origin.longitude * M_PI / 180

    let lat2 = asin(sin(lat1) * cos(distRadians) + cos(lat1) * sin(distRadians) * cos(bearingRadians))
    let lon2 = lon1 + atan2(sin(bearingRadians) * sin(distRadians) * cos(lat1), cos(distRadians) - sin(lat1) * sin(lat2))

    return CLLocationCoordinate2D(latitude: lat2 * 180 / M_PI, longitude: lon2 * 180 / M_PI)
}

Morgan Chen写道:

该方法中的所有数学计算都是用弧度完成的。在此目的下,lon1和lat1在方法开始时也被转换为弧度。方位角也是以弧度表示的。请记住,该方法考虑了地球的曲率,但对于小距离您不必这样做。

我的评论(2021年3月25日):

在C.F.F. Karney的文章“测地线算法”(2012年)中讨论了在该方法中使用的计算称为求解“直接测地问题”的技术。上面给出的代码使用的技术比Karney文章中介绍的算法要不精确一些。


亲爱的Peter,感谢您迅速解决了我的问题,但是我需要更正一件事情。轴承应该是弧度而不是双精度数。由于我无法在此处粘贴代码,因此我将其发布为答案。 - Koray Birand
18
如果有人不清楚,这里的“bearing”指的是你想要前进的方向,用度数表示。例如,北方为0度,东方为90度,西南方为225度,依此类推... - Shaked Sayag
需要注意的是,它不会进行换行(即181经度不会转换为-181)。 - InkGolem
@ShakedSayag 注意,当前答案的方位角是以弧度为单位的,因此北方的方位角为0,东方的方位角为0.5π,南方的方位角为π等。 - Freek Sanders
速度非常快,准确率非常高。不错的解决方案。谢谢! - nurider

23

对Peter答案进行了改进的swift解决方案。唯一的更正是计算时应该使用弧度而不是角度。

 func locationWithBearing(bearing:Double, distanceMeters:Double, origin:CLLocationCoordinate2D) -> CLLocationCoordinate2D {
    let distRadians = distanceMeters / (6372797.6)

    var rbearing = bearing * M_PI / 180.0

    let lat1 = origin.latitude * M_PI / 180
    let lon1 = origin.longitude * M_PI / 180

    let lat2 = asin(sin(lat1) * cos(distRadians) + cos(lat1) * sin(distRadians) * cos(rbearing))
    let lon2 = lon1 + atan2(sin(rbearing) * sin(distRadians) * cos(lat1), cos(distRadians) - sin(lat1) * sin(lat2))

    return CLLocationCoordinate2D(latitude: lat2 * 180 / M_PI, longitude: lon2 * 180 / M_PI)
}

我已经尝试了你的解决方案。大部分情况下它运行良好,但也存在一些偏差。为什么会这样呢?在这里提问:http://stackoverflow.com/questions/36382149/moving-gmsmarker-by-x-meters-deflects - Can
我几乎有相同的实现(我的实现与所有答案都相等),但是我在反转位置时遇到了问题!据我所知,我们应该添加-(距离),但它会出现错误(在小数和指向稍远的地方)并且我无法再次提取原始位置!有什么想法吗? - Mo Farhand

16

很棒的帖子,这里是给那些喜欢复制粘贴的人的Obj-C封装:

- (CLLocationCoordinate2D) locationWithBearing:(float)bearing distance:(float)distanceMeters fromLocation:(CLLocationCoordinate2D)origin {
    CLLocationCoordinate2D target;
    const double distRadians = distanceMeters / (6372797.6); // earth radius in meters

    float lat1 = origin.latitude * M_PI / 180;
    float lon1 = origin.longitude * M_PI / 180;

    float lat2 = asin( sin(lat1) * cos(distRadians) + cos(lat1) * sin(distRadians) * cos(bearing));
    float lon2 = lon1 + atan2( sin(bearing) * sin(distRadians) * cos(lat1),
                     cos(distRadians) - sin(lat1) * sin(lat2) );

    target.latitude = lat2 * 180 / M_PI;
    target.longitude = lon2 * 180 / M_PI; // no need to normalize a heading in degrees to be within -179.999999° to 180.00000°

    return target;
}

无法正常工作。在相同方位角下,给出不同距离的不同位置。 - kirander
在车辆行驶时,如果您想要知道精确时刻的位置和距离,另一个有用的补充/改进是根据移动车辆速度的估计计算自上次位置以来的距离。我认为您可以通过计算GPS信号的时间戳变量与设备时钟时间之间的延迟来实现这一点。苹果设备以1赫兹的频率采样GPS位置,但具有与CLLocation交互的API的第三方GPS接收器以4到10赫兹进行采样(例如Dual 150和160型号)。 - Michael Young

6

有一个 C 函数与你的需求接近,但它需要方位角和距离。可以在我的 github 的 UtilitiesGeo 类中找到该函数。你可以将 CLLocation 中的纬度和经度传递给它,然后从返回的 lat2 和 lon2 创建新的 CLLocation:

/*-------------------------------------------------------------------------
* Given a starting lat/lon point on earth, distance (in meters)
* and bearing, calculates destination coordinates lat2/lon2.
*
* all params in degrees
*-------------------------------------------------------------------------*/
void destCoordsInDegrees(double lat1, double lon1,
                         double distanceMeters, double bearing,
                         double* lat2, double* lon2);

如果您无法使用此方法,请查看其衍生算法(此处)(此处),也许您可以修改它或这些网站可能更适合您的需要。

看起来非常非常接近(事实上几乎完美)我正在寻找的东西。承载参数会有帮助 :-) 我今晚会尝试一下。 - Oliver
这个函数使用distanceMeters作为球面上的米,还是通过球体? - Oliver
为什么要对经度进行归一化而不是纬度? - Oliver
normalize180 将经度规范化为始终在-180到+180的范围内。 - progrmr
我几乎有相同的实现(我的实现与所有答案都相等),但是我在反转位置时遇到了问题!据我所知,我们应该添加-(距离),但它会出现错误(在小数和指向稍远的地方)并且我无法再次提取原始位置!有什么想法吗? - Mo Farhand

6

奇怪的是,没人想到使用 MapKit 中的 MKCoordinateRegion 来自动计算。

import MapKit

extension CLLocation {
    func movedBy(latitudinalMeters: CLLocationDistance, longitudinalMeters: CLLocationDistance) -> CLLocation {
        let region = MKCoordinateRegion(center: coordinate, latitudinalMeters: abs(latitudinalMeters), longitudinalMeters: abs(longitudinalMeters))

        let latitudeDelta = region.span.latitudeDelta
        let longitudeDelta = region.span.longitudeDelta

        let latitudialSign = CLLocationDistance(latitudinalMeters.sign == .minus ? -1 : 1)
        let longitudialSign = CLLocationDistance(longitudinalMeters.sign == .minus ? -1 : 1)

        let newLatitude = coordinate.latitude + latitudialSign * latitudeDelta
        let newLongitude = coordinate.longitude + longitudialSign * longitudeDelta

        let newCoordinate = CLLocationCoordinate2D(latitude: newLatitude, longitude: newLongitude)

        let newLocation = CLLocation(coordinate: newCoordinate, altitude: altitude, horizontalAccuracy: horizontalAccuracy, verticalAccuracy: verticalAccuracy, course: course, speed: speed, timestamp: Date())

        return newLocation
    }
}

4
稍微调整一下@CocoaChris的答案:现在CLLocation有一个类别,并且使用内置单位。
#import <CoreLocation/CoreLocation.h>


@interface CLLocation (Movement)

- (CLLocation *)locationByMovingDistance:(double)distanceMeters withBearing:(CLLocationDirection)bearingDegrees;

@end


@implementation CLLocation (Movement)

- (CLLocation *)locationByMovingDistance:(double)distanceMeters withBearing:(CLLocationDirection)bearingDegrees
{
    const double distanceRadians = distanceMeters / (6372797.6); // earth radius in meters
    const double bearingRadians = bearingDegrees * M_PI / 180;

    float lat1 = self.coordinate.latitude * M_PI / 180;
    float lon1 = self.coordinate.longitude * M_PI / 180;

    float lat2 = asin(sin(lat1) * cos(distanceRadians) + cos(lat1) * sin(distanceRadians) * cos(bearingRadians));
    float lon2 = lon1 + atan2(sin(bearingRadians) * sin(distanceRadians) * cos(lat1),
                              cos(distanceRadians) - sin(lat1) * sin(lat2) );

    return [[CLLocation alloc] initWithLatitude:lat2 * 180 / M_PI
                                      longitude:lon2 * 180 / M_PI];
}

@end

我几乎有相同的实现(我的实现与所有答案相等),但是我在反转位置时遇到了问题!据我所知,我们应该添加-(距离),但它会出现错误(在小数和指向稍远位置的点上),我无法再提取原始位置!有什么想法吗? - Mo Farhand
嗨@Mohamad,这可能是一个数字精度错误,你尝试过使用double而不是float吗? - joerick
嗨 @joerick, 我正在使用 double,在参考网站 https://www.movable-type.co.uk/scripts/latlong.html 上也出现了同样的问题。 “给定起点距离和方位角的终点” 我不知道是什么问题。 - Mo Farhand

4

更简单的解决方案是使用MKMapPoints。

使用以下方法将您的原始坐标和任何偏移距离转换为MKMapPoints:

let coordinatesInMapPoints = MKMapPointForCoordinate(CLLocationCoordinate2D)
let distancesInMapPoints = yourDistanceInMeters * MKMapPointsPerMeterAtLatitude(CLLocationDegrees) // Do this for both x and y directions if needed.

然后,通过将偏移距离加到原始坐标上,简单地创建一个新的MKMapPoint:

let newCoordinatesInMapPoints = MKMapPointMake(coordinatesInMapPoints.x + distancesInMapPoints, coordinatesInMapPoints.y)

最后,将新坐标从MKMapPoint转换回CLLocationCoordinate2D:
let newCoordinate = MKCoordinateForMapPoint(newCoordinatesInMapPoints)

不需要复杂的转换计算。

如何在y方向上实现? - Sushil Sharma
如果偏移距离不同,则使用不同的“yourDistanceInMeters”重复第2行,并将其添加到“coordinatesInMapPoints.y”中。 如果偏移距离相同,则只需将“distancesInMapPoints”添加到“coordinatesInMapPoints.y”中即可。 - hubear

3

使用Measurement结构实现Swift代码,用于在度和弧度之间进行转换。

class GPSLocation {

public class func degreesToRadians(degrees: Double) -> Double {
        return Measurement(value: degrees, unit: UnitAngle.degrees).converted(to: .radians).value
    }

    public class func radiansToDegrees(radians: Double) -> Double {
        return Measurement(value: radians, unit: UnitAngle.radians).converted(to: .degrees).value
    }

    public class func location(location: CLLocation, byMovingDistance distance: Double, withBearing bearingDegrees:CLLocationDirection) -> CLLocation {
        let distanceRadians: Double = distance / 6372797.6
        let bearingRadians: Double = GPSLocation.degreesToRadians(degrees: bearingDegrees)

        let lat1 = GPSLocation.degreesToRadians(degrees: location.coordinate.latitude)
        let lon1 = GPSLocation.degreesToRadians(degrees: location.coordinate.longitude)

        let lat2 = GPSLocation.radiansToDegrees(radians:asin(sin(lat1) * cos(distanceRadians) + cos(lat1) * sin(distanceRadians) * cos(bearingRadians)))
        let lon2 = GPSLocation.radiansToDegrees(radians:lon1 + atan2(sin(bearingRadians) * sin(distanceRadians * cos(lat1)), cos(distanceRadians) - sin(lat1) * sin(lat2)))

        return CLLocation(latitude: lat2, longitude: lon2)
    }

}

1

Swift 4

extension CLLocationCoordinate2D {

    /// Get coordinate moved from current to `distanceMeters` meters with azimuth `azimuth` [0, Double.pi)
    ///
    /// - Parameters:
    ///   - distanceMeters: the distance in meters
    ///   - azimuth: the azimuth (bearing)
    /// - Returns: new coordinate
    func shift(byDistance distanceMeters: Double, azimuth: Double) -> CLLocationCoordinate2D {
        let bearing = azimuth
        let origin = self
        let distRadians = distanceMeters / (6372797.6) // earth radius in meters

        let lat1 = origin.latitude * Double.pi / 180
        let lon1 = origin.longitude * Double.pi / 180

        let lat2 = asin(sin(lat1) * cos(distRadians) + cos(lat1) * sin(distRadians) * cos(bearing))
        let lon2 = lon1 + atan2(sin(bearing) * sin(distRadians) * cos(lat1), cos(distRadians) - sin(lat1) * sin(lat2))
        return CLLocationCoordinate2D(latitude: lat2 * 180 / Double.pi, longitude: lon2 * 180 / Double.pi)
    }
}

使用
    let point: CLLocationCoordinate2D!
    let north100 = point.shift(byDistance: 100, azimuth: 0) // 100m to North
    let south100 = point.shift(byDistance: 100, azimuth: Double.pi) // 100m to South

这个答案非常清晰。您可以分享“向东100米”和“向西100米”的方位角数值吗? - thus
1
@因此,西方= pi/2,东方= -pi/2 - Alexander Volkov
对于任何角度[方位角],它是否能正确工作? - Santosh Singh

1

作为CGPoint扩展的Swift 4.2

源自Peter O.的解决方案

FloatingPoint扩展:感谢https://dev59.com/X14b5IYBdhLWcg3waA5P#29179878

extension FloatingPoint
{
    var degreesToRadians: Self { return self * .pi / 180 }
    var radiansToDegrees: Self { return self * 180 / .pi }
}

extension CGPoint
{
    // NOTE: bearing is in radians
    func locationWithBearing(bearing: Double, distanceMeters: Double) -> CGPoint
    {
        let distRadians = distanceMeters / (6372797.6) // earth radius in meters

        let origLat = Double(self.y.degreesToRadians)
        let origLon = Double(self.x.degreesToRadians)

        let newLat = asin(sin(origLat) * cos(distRadians) + cos(origLat) * sin(distRadians) * cos(bearing))
        let newLon = origLon + atan2(sin(bearing) * sin(distRadians) * cos(origLat), cos(distRadians) - sin(origLat) * sin(newLat))

        return CGPoint(x: newLon.radiansToDegrees, y: newLat.radiansToDegrees)
    }
}

使用方法:

let loc = CGPoint(x: lon, y: lat)
let newLoc = loc.locationWithBearing(bearing: 90.degreesToRadians, distanceMeters: 500.0)

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接