在半径范围内随机生成经纬度会导致超出边界的点

4

我正在尝试在一个半径内生成一个点,但是我得到的值是不正确的。请有人帮忙看一下并告诉我经度出了问题吗?这是另一个问题上发布的公式方法...

  public static Location generateLocationWithinRadius(Location myCurrentLocation) {
    return getLocationInLatLngRad(1000, myCurrentLocation);
}

protected static Location getLocationInLatLngRad(double radiusInMeters, Location currentLocation) {
    double x0 = currentLocation.getLatitude();
    double y0 = currentLocation.getLongitude();

    Random random = new Random();

    // Convert radius from meters to degrees
    double radiusInDegrees = radiusInMeters / 111000f;

    double u = random.nextDouble();
    double v = random.nextDouble();
    double w = radiusInDegrees * Math.sqrt(u);
    double t = 2 * Math.PI * v;
    double x = w * Math.cos(t);
    double y = w * Math.sin(t);

    double new_x = x / Math.cos(y0);
    double new_y = y / Math.cos(x0);
    double foundLatitude;
    double foundLongitude;
    boolean shouldAddOrSubtractLat = random.nextBoolean();
    boolean shouldAddOrSubtractLon = random.nextBoolean();
    if (shouldAddOrSubtractLat) {
        foundLatitude = new_x + x0;
    } else {
        foundLatitude = x0 - new_x;
    }
    if (shouldAddOrSubtractLon) {
        foundLongitude = new_y + y0;
    } else {
        foundLongitude = y0 - new_y;
    }
    Location copy = new Location(currentLocation);
    copy.setLatitude(foundLatitude);
    copy.setLongitude(foundLongitude);
    return copy;
}

我还应该提到的是,由于某种原因,当查看有效点时,它们会产生一条均匀的坐标线。

我认为纬度处理正确,而经度则没有。


1
显示您的输入、输出和期望输出。 - shmosel
我应该也说一下,我现在正在使用静态值,但我将使它们可调整并减少指针的数量。 - KoalaKoalified
经度和纬度在小数位上变化很大。因此,如果它们被限制在一个半径内,那么您生成的随机值应该在0.000x范围内有所不同。 - Vivek Mishra
目前纬度可以正常工作,但经度无法正确工作。如果我只改变纬度值,那么点会分布在一条直线上,这是可以接受的,但我更希望点在经度方向上分散。我想使用最大/最小值来解决这个问题。 - KoalaKoalified
请问您能否说明一下Location类是什么?我猜它是由两个double类型的(纬度,经度)组成的一对,但我想确认一下。 - Julian
显示剩余3条评论
4个回答

20
您的代码似乎基本上是基于一个在gis.stackexchange.com上提出并在此讨论此讨论中进一步讨论的想法。
如果我们根据这些讨论仔细研究它,也许会更有意义。
为了简单地将值限制在圆圈内,它采用随机化方向和距离的方法。首先,我们获取两个0.0 ... 1.0之间的随机双精度值:
double u = random.nextDouble();
double v = random.nextDouble();

由于半径以米为单位,而计算需要角度,因此进行转换:

double radiusInDegrees = radiusInMeters / 111000f;

这里使用赤道的度数与米数比例。(维基百科建议为111320米。)
为了让随机点更加均匀分布,距离会通过平方根进行补偿:
w = r * sqrt(u)

否则,中心附近和远离中心的点数就会存在统计偏差。1的平方根是1,0的平方根当然是0,因此将随机双倍数的根乘以预期的最大半径始终会得到0到半径之间的值。
然后,另一个随机双倍数乘以2 * pi,因为一个完整的圆中有2 * pi弧度:
t = 2 * Pi * v

现在我们有一个角度,介于0到2 * pi之间,即0到360度。

然后使用基本三角函数和随机距离和随机角度计算出随机x和y坐标增量:

x = w * cos(t) 
y = w * sin(t)
< p > [x,y] 然后指向原始坐标沿着方向t随机距离w

然后,经过三角函数的修正(其中y0是中心点的y坐标),经线之间的变化距离被补偿:

x' = x / cos(y0)

cos() 函数期望角度为弧度,则需要将以上的 y0 转换为弧度。在 Java 中,它是这样的。

然后建议将这些增量值添加到原始坐标中。对于整个圆的一半角度而言,cossin 值为负数,因此只需进行加法运算即可。一些随机点将位于格林威治以西和赤道以南,无需随机化进行加减运算。

所以随机点将位于 (x'+x0, y+y0)

我不知道你的代码为什么有:

double new_y = y / Math.cos(x0);

就像之前提到的,我们可以忽略shouldAddOrSubtractLatshouldAddOrSubtractLon

在我看来,x指的是从左到右或从西到东的移动。这就是为什么经度值增加,即使经线是从南向北的。因此,让我们将x用作经度,将y用作纬度。

那么还剩下什么呢?类似于:

protected static Location getLocationInLatLngRad(double radiusInMeters, Location currentLocation) {
    double x0 = currentLocation.getLongitude();
    double y0 = currentLocation.getLatitude();

    Random random = new Random();

    // Convert radius from meters to degrees.
    double radiusInDegrees = radiusInMeters / 111320f;

    // Get a random distance and a random angle.
    double u = random.nextDouble();
    double v = random.nextDouble();
    double w = radiusInDegrees * Math.sqrt(u);
    double t = 2 * Math.PI * v;
    // Get the x and y delta values.
    double x = w * Math.cos(t);
    double y = w * Math.sin(t);

    // Compensate the x value.
    double new_x = x / Math.cos(Math.toRadians(y0));

    double foundLatitude;
    double foundLongitude;

    foundLatitude = y0 + y;
    foundLongitude = x0 + new_x;

    Location copy = new Location(currentLocation);
    copy.setLatitude(foundLatitude);
    copy.setLongitude(foundLongitude);
    return copy;
}

1
在SOF上曾经给出的最好的答案之一! - Nirmalya
在这行代码中,'f'代表什么意思? // double radiusInDegrees = radiusInMeters / 111320f; - Balaji Ks
这意味着111320是一个浮点数。 - Markus Kauppinen

2

这是Markus Kauppinen的答案的Kotlin版本。

fun Location.getRandomLocation(radius: Double): Location {
    val x0: Double = longitude
    val y0: Double = latitude

    // Convert radius from meters to degrees.

    // Convert radius from meters to degrees.
    val radiusInDegrees: Double = radius / 111320f

    // Get a random distance and a random angle.

    // Get a random distance and a random angle.
    val u = Random.nextDouble()
    val v = Random.nextDouble()
    val w = radiusInDegrees * sqrt(u)
    val t = 2 * Math.PI * v
    // Get the x and y delta values.
    // Get the x and y delta values.
    val x = w * cos(t)
    val y = w * sin(t)

    // Compensate the x value.

    // Compensate the x value.
    val newX = x / cos(Math.toRadians(y0))

    val foundLatitude: Double
    val foundLongitude: Double

    foundLatitude = y0 + y
    foundLongitude = x0 + newX

    val copy = Location(this)
    copy.latitude = foundLatitude
    copy.longitude = foundLongitude
    return copy
}

1
很难为您提供一个纯Android解决方案,因为我从未使用过那些API。但是我确信您可以轻松地调整此解决方案,以生成给定半径内现有点的随机点。
问题在二维空间中得到解决,但是很容易扩展以支持海拔高度。
请查看下面的代码。它为您提供了一个LocationGenerator以及我自己的Location实现和一个单元测试,证明它可行。
我的解决方案基于解决圆形方程(x-a)^2 + (y-b)^2 = r^2
package my.test.pkg;

import org.junit.Test;

import java.util.Random;

import static org.junit.Assert.assertTrue;

public class LocationGeneratorTest {
    private class Location {
        double longitude;
        double latitude;

        public Location(double longitude, double latitude) {
            this.longitude = longitude;
            this.latitude = latitude;
        }
    }

    private class LocationGenerator {
        private final Random random = new Random();

        Location generateLocationWithinRadius(Location currentLocation, double radius) {
            double a = currentLocation.longitude;
            double b = currentLocation.latitude;
            double r = radius;

            // x must be in (a-r, a + r) range
            double xMin = a - r;
            double xMax = a + r;
            double xRange = xMax - xMin;

            // get a random x within the range
            double x = xMin + random.nextDouble() * xRange;

            // circle equation is (y-b)^2 + (x-a)^2 = r^2
            // based on the above work out the range for y
            double yDelta = Math.sqrt(Math.pow(r,  2) - Math.pow((x - a), 2));
            double yMax = b + yDelta;
            double yMin = b - yDelta;
            double yRange = yMax - yMin;
            // Get a random y within its range
            double y = yMin + random.nextDouble() * yRange;

            // And finally return the location
            return new Location(x, y);
        }
    }

    @Test
    public void shoulRandomlyGeneratePointWithinRadius () throws Exception {
        LocationGenerator locationGenerator = new LocationGenerator();
        Location currentLocation = new Location(20., 10.);
        double radius = 5.;
        for (int i=0; i < 1000000; i++) {
            Location randomLocation = locationGenerator.generateLocationWithinRadius(currentLocation, radius);
            try {
                assertTrue(Math.pow(randomLocation.latitude - currentLocation.latitude, 2) + Math.pow(randomLocation.longitude - currentLocation.longitude, 2) < Math.pow(radius, 2));
            } catch (Throwable e) {
                System.out.println("i= " + i + ", x=" + randomLocation.longitude + ", y=" + randomLocation.latitude);
                throw new Exception(e);
            }
        }

    }
}

注意: 这只是一个通用的解决方案,用于获取以(a, b)为圆心,半径为r的圆内的随机点,可用于解决您的问题,但不是您可以直接使用的简单解决方案。您很可能需要根据您的使用情况进行调整。

我认为这是一个自然的解决方案。

敬礼


我应该使用什么单位来表示半径?我尝试过米和千米,但是得到的当前位置和目标位置之间的距离非常大。 - KoalaKoalified
半径显然必须以度为单位,因为它要加到/从以度为单位的经度值中减去。因此,它必须从米转换为度才能调用函数。我想知道在这些计算中角度/米比率在南北方向上改变的影响是什么。(也许它可以正常工作。我没有检查过。) - Markus Kauppinen
@KoalaKoalified 在我的算法中,我没有附加任何距离单位的概念。它们只是三个双精度值:经度、纬度和半径。当我实现它时,我的假设是它们只是用相同单位表示的长度。根据您的用例,您可以用米或千米表示。如果您的代码使用度数,我认为将它们转换为米或千米取决于您的需求不会有任何问题。请记住,我的位置类只是为了让生成器工作而虚构的,可能与您正在使用的位置类非常不同。 - Julian
我相信有很多科学场所可以提供正确的转换公式。这里只列出其中两个: 这里这里 看起来相当复杂,如果没有一些已经完成此任务的Java API,我会感到惊讶。 - Julian

0
经纬度使用椭球坐标,因此对于大半径(百米级别),使用此方法的误差会变得显着。一个可能的技巧是将其转换为笛卡尔坐标,进行半径随机化,然后再次转换为椭球坐标用于经纬度。我已经使用ibm的this java library测试了几公里,并取得了巨大的成功。更长的距离可能也行,但最终半径会因地球呈现其球形而逐渐减小。

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