Index: /trunk/src/org/openstreetmap/josm/tools/Geometry.java
===================================================================
--- /trunk/src/org/openstreetmap/josm/tools/Geometry.java	(revision 18108)
+++ /trunk/src/org/openstreetmap/josm/tools/Geometry.java	(revision 18109)
@@ -1,4 +1,6 @@
 // License: GPL. For details, see LICENSE file.
 package org.openstreetmap.josm.tools;
+
+import static org.openstreetmap.josm.data.projection.Ellipsoid.WGS84;
 
 import java.awt.geom.Area;
@@ -26,4 +28,5 @@
 import org.openstreetmap.josm.data.coor.EastNorth;
 import org.openstreetmap.josm.data.coor.ILatLon;
+import org.openstreetmap.josm.data.coor.LatLon;
 import org.openstreetmap.josm.data.osm.BBox;
 import org.openstreetmap.josm.data.osm.DataSet;
@@ -1557,4 +1560,27 @@
 
     /**
+     * Create a new LatLon at a specified distance. Currently uses WGS84, but may change randomly in the future.
+     * This does not currently attempt to be hugely accurate. The actual location may be off
+     * depending upon the distance and the elevation, but should be within 0.0002 meters.
+     *
+     * @param original The originating point
+     * @param angle The angle (from true north) in radians
+     * @param offset The distance to the new point in the current projection's units
+     * @return The location at the specified angle and distance from the originating point
+     * @since 18109
+     */
+    public static ILatLon getLatLonFrom(final ILatLon original, final double angle, final double offset) {
+        final double meterOffset = ProjectionRegistry.getProjection().getMetersPerUnit() * offset;
+        final double radianLat = Math.toRadians(original.lat());
+        final double radianLon = Math.toRadians(original.lon());
+        final double angularDistance = meterOffset / WGS84.a;
+        final double lat = Math.asin(Math.sin(radianLat) * Math.cos(angularDistance)
+                + Math.cos(radianLat) * Math.sin(angularDistance) * Math.cos(angle));
+        final double lon = radianLon + Math.atan2(Math.sin(angle) * Math.sin(angularDistance) * Math.cos(radianLat),
+                Math.cos(angularDistance) - Math.sin(radianLat) * Math.sin(lat));
+        return new LatLon(Math.toDegrees(lat), Math.toDegrees(lon));
+    }
+
+    /**
      * Calculate closest distance between a line segment s1-s2 and a point p
      * @param s1 start of segment
Index: /trunk/test/unit/org/openstreetmap/josm/TestUtils.java
===================================================================
--- /trunk/test/unit/org/openstreetmap/josm/TestUtils.java	(revision 18108)
+++ /trunk/test/unit/org/openstreetmap/josm/TestUtils.java	(revision 18109)
@@ -23,4 +23,5 @@
 import java.time.format.DateTimeFormatter;
 import java.time.temporal.Temporal;
+import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
@@ -31,6 +32,8 @@
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.ThreadPoolExecutor;
+import java.util.concurrent.atomic.AtomicInteger;
 import java.util.function.Function;
 import java.util.stream.Collectors;
+import java.util.stream.IntStream;
 import java.util.stream.Stream;
 
@@ -169,4 +172,44 @@
     }
 
+    /**
+     * Create a test matrix for parameterized tests.
+     * <br />
+     * <b>WARNING:</b> This can quickly become <i>very</i> large (this is combinatorial,
+     * so the returned {@link Stream} length is the size of the object collections multiplied by each other.
+     * So if you have three lists of size 3, 4, and 5, the stream size would be {@code 3 * 4 * 5} or 60 elements.
+     * <br />
+     * Generally speaking, you should avoid putting expected values into the test matrix.
+     *
+     * @param objectCollections The collections of objects. May include/provide {@code null}.
+     * @return The object arrays to be used as arguments. Note: The returned stream might not be thread-safe.
+     */
+    public static Stream<Object[]> createTestMatrix(List<?>... objectCollections) {
+        // Create the original object arrays
+        final AtomicInteger size = new AtomicInteger(1);
+        Stream.of(objectCollections).mapToInt(Collection::size).forEach(i -> size.set(size.get() * i));
+        final List<Object[]> testMatrix = new ArrayList<>(size.get());
+        final int[] indexes = IntStream.range(0, objectCollections.length).map(i -> 0).toArray();
+
+        // It is important to make a new object array each time (we modify them)
+        return IntStream.range(0, size.get()).mapToObj(index -> new Object[objectCollections.length]).peek(args -> {
+            // Just in case someone tries to make this parallel, synchronize on indexes to avoid most issues.
+            synchronized (indexes) {
+                // Set the args
+                for (int listIndex = 0; listIndex < objectCollections.length; listIndex++) {
+                    args[listIndex] = objectCollections[listIndex].get(indexes[listIndex]);
+                }
+                // Increment indexes
+                for (int listIndex = 0; listIndex < objectCollections.length; listIndex++) {
+                    indexes[listIndex] = indexes[listIndex] + 1;
+                    if (indexes[listIndex] >= objectCollections[listIndex].size()) {
+                        indexes[listIndex] = 0;
+                    } else {
+                        break;
+                    }
+                }
+            }
+        });
+    }
+
     private static <T> String getFailMessage(T o1, T o2, int a, int b) {
         return new StringBuilder("Compared\no1: ").append(o1).append("\no2: ")
Index: /trunk/test/unit/org/openstreetmap/josm/tools/GeometryTest.java
===================================================================
--- /trunk/test/unit/org/openstreetmap/josm/tools/GeometryTest.java	(revision 18108)
+++ /trunk/test/unit/org/openstreetmap/josm/tools/GeometryTest.java	(revision 18109)
@@ -12,9 +12,14 @@
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.Collections;
 import java.util.List;
+import java.util.stream.Stream;
 
 import org.junit.Assert;
+import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.extension.RegisterExtension;
-import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
 import org.openstreetmap.josm.TestUtils;
 import org.openstreetmap.josm.data.coor.EastNorth;
@@ -27,4 +32,7 @@
 import org.openstreetmap.josm.data.osm.Way;
 import org.openstreetmap.josm.data.osm.search.SearchCompiler;
+import org.openstreetmap.josm.data.projection.Projection;
+import org.openstreetmap.josm.data.projection.ProjectionRegistry;
+import org.openstreetmap.josm.data.projection.Projections;
 import org.openstreetmap.josm.io.OsmReader;
 import org.openstreetmap.josm.testutils.JOSMTestRules;
@@ -470,3 +478,36 @@
     }
 
+    static Stream<Arguments> testGetLatLonFrom() {
+        // The projection can quickly explode the test matrix, so only test WGS84 (EPSG:3857). If other projections have
+        // issues, add them to the first list.
+        return TestUtils.createTestMatrix(
+                // Check specific projections
+                Collections.singletonList(Projections.getProjectionByCode("EPSG:3857")),
+                // Check extreme latitudes (degrees)
+                Arrays.asList(0, 89, -89),
+                // Test extreme longitudes (degrees)
+                Arrays.asList(0, -179, 179),
+                // Test various angles (degrees)
+                // This tests cardinal directions, and then some varying angles.
+                // TBH, the cardinal directions should find any issues uncovered by the varying angles,
+                // but it may not.
+                Arrays.asList(0, 90, 180, 270, 45),
+                // Test various distances (meters)
+                Arrays.asList(1, 10_000)
+                ).map(Arguments::of);
+    }
+
+    @ParameterizedTest(name = "[{index}] {3}Â° {4}m @ lat = {1} lon = {2} - {0}")
+    @MethodSource
+    void testGetLatLonFrom(final Projection projection, final double lat, final double lon, final double angle, final double offsetInMeters) {
+        ProjectionRegistry.setProjection(projection);
+        final double offset = offsetInMeters / projection.getMetersPerUnit();
+        final LatLon original = new LatLon(lat, lon);
+
+        final LatLon actual = (LatLon) Geometry.getLatLonFrom(original, Math.toRadians(angle), offset);
+        // Due to degree -> radian -> degree conversion, there is a limit to how precise it can be
+        assertEquals(offsetInMeters, original.greatCircleDistance(actual), 0.000_000_1);
+        // The docs indicate that this should not be highly precise.
+        assertEquals(angle, Math.toDegrees(original.bearing(actual)), 0.000_001);
+    }
 }
