diff --git a/src/org/openstreetmap/josm/actions/AlignInLineAction.java b/src/org/openstreetmap/josm/actions/AlignInLineAction.java index 9d85c4f565e..55d6fc61564 100644 --- a/src/org/openstreetmap/josm/actions/AlignInLineAction.java +++ b/src/org/openstreetmap/josm/actions/AlignInLineAction.java @@ -2,6 +2,7 @@ package org.openstreetmap.josm.actions; import static org.openstreetmap.josm.gui.help.HelpUtil.ht; +import static org.openstreetmap.josm.tools.Geometry.nodePairFurthestApart; import static org.openstreetmap.josm.tools.I18n.tr; import java.awt.event.ActionEvent; @@ -80,91 +81,6 @@ static class InvalidSelection extends Exception { } } - /** - * Return 2 nodes making up the line along which provided nodes must be aligned. - * - * @param nodes Nodes to be aligned. - * @return A array of two nodes. - * @throws IllegalArgumentException if nodes is empty - */ - private static Node[] nodePairFurthestApart(List nodes) { - // Detect if selected nodes are on the same way. - - // Get ways passing though all selected nodes. - Set waysRef = null; - for (Node n: nodes) { - Collection ref = n.getParentWays(); - if (waysRef == null) - waysRef = new HashSet<>(ref); - else - waysRef.retainAll(ref); - } - - if (waysRef == null) { - throw new IllegalArgumentException(); - } - - // Nodes belongs to multiple ways, return most distant nodes. - if (waysRef.size() != 1) - return nodeFurthestAppart(nodes); - - // All nodes are part of the same way. See #9605. - Way way = waysRef.iterator().next(); - - if (way.isClosed()) { - // Align these nodes on the line passing through the most distant nodes. - return nodeFurthestAppart(nodes); - } - - Node nodea = null; - Node nodeb = null; - - // The way is open, align nodes on the line passing through the extremity nodes (most distant in the way - // sequence). See #9605#comment:3. - Set remainNodes = new HashSet<>(nodes); - for (Node n : way.getNodes()) { - if (!remainNodes.contains(n)) - continue; - if (nodea == null) - nodea = n; - if (remainNodes.size() == 1) { - nodeb = remainNodes.iterator().next(); - break; - } - remainNodes.remove(n); - } - - return new Node[] {nodea, nodeb}; - } - - /** - * Return the two nodes the most distant from the provided list. - * - * @param nodes List of nodes to analyze. - * @return An array containing the two most distant nodes. - */ - private static Node[] nodeFurthestAppart(List nodes) { - Node node1 = null, node2 = null; - double minSqDistance = 0; - int nb; - - nb = nodes.size(); - for (int i = 0; i < nb - 1; i++) { - Node n = nodes.get(i); - for (int j = i + 1; j < nb; j++) { - Node m = nodes.get(j); - double sqDist = n.getEastNorth().distanceSq(m.getEastNorth()); - if (sqDist > minSqDistance) { - node1 = n; - node2 = m; - minSqDistance = sqDist; - } - } - } - - return new Node[] {node1, node2}; - } - /** * Operation depends on the selected objects: */ @@ -190,7 +106,7 @@ public void actionPerformed(ActionEvent e) { * Builds "align in line" command depending on the selected objects. * @param ds data set in which the command operates * @return the resulting command to execute to perform action - * @throws InvalidSelection if a polygon is selected, or if a node is used by 3 or more ways + * @throws InvalidSelection if a polygon is selected, or if a node is used by three or more ways * @since 13108 */ public Command buildCommand(DataSet ds) throws InvalidSelection { @@ -226,7 +142,7 @@ public Command buildCommand(DataSet ds) throws InvalidSelection { } /** - * Align nodes in case 3 or more nodes are selected. + * Align nodes in case three or more nodes are selected. * * @param nodes Nodes to be aligned. * @return Command that perform action. diff --git a/src/org/openstreetmap/josm/actions/DistributeAction.java b/src/org/openstreetmap/josm/actions/DistributeAction.java index 75f2c766814..cc63aeb024d 100644 --- a/src/org/openstreetmap/josm/actions/DistributeAction.java +++ b/src/org/openstreetmap/josm/actions/DistributeAction.java @@ -2,10 +2,13 @@ package org.openstreetmap.josm.actions; import static org.openstreetmap.josm.gui.help.HelpUtil.ht; +import static org.openstreetmap.josm.tools.Geometry.getFurthestPrimitive; +import static org.openstreetmap.josm.tools.Geometry.nodeFurthestApart; import static org.openstreetmap.josm.tools.I18n.tr; import java.awt.event.ActionEvent; import java.awt.event.KeyEvent; +import java.util.ArrayList; import java.util.Collection; import java.util.HashSet; import java.util.Iterator; @@ -49,7 +52,9 @@ public DistributeAction() { * Select method according to user selection. * Case 1: One Way (no self-crossing) and at most 2 nodes contains by this way: * Distribute nodes keeping order along the way - * Case 2: Other + * Case 2: One Node part of at least one way, not a start or end node + * Distribute the selected node relative to neighbors + * Case 3: Other * Distribute nodes */ @Override @@ -59,8 +64,8 @@ public void actionPerformed(ActionEvent e) { // Collect user selected objects Collection selected = getLayerManager().getEditDataSet().getSelected(); - Collection ways = new LinkedList<>(); - Collection nodes = new HashSet<>(); + List ways = new ArrayList<>(); + List nodes = new ArrayList<>(); for (OsmPrimitive osm : selected) { if (osm instanceof Node) { nodes.add((Node) osm); @@ -81,13 +86,15 @@ public void actionPerformed(ActionEvent e) { cmds = distributeWay(ways, nodes); } else if (checkDistributeNodes(ways, nodes)) { cmds = distributeNodes(nodes); + } else if (checkDistributeNode(nodes)) { + cmds = distributeNode(nodes); } else { new Notification( - tr("Please select :\n" + - "* One no self-crossing way with at most two of its nodes;\n" + - "* Three nodes.")) + tr("Please select:
    " + + "
  • One no self-crossing way with at most two of its nodes;
  • " + + "
  • One node in the middle of a way;
  • " + + "
  • Three nodes.
")) .setIcon(JOptionPane.INFORMATION_MESSAGE) - .setDuration(Notification.TIME_SHORT) .show(); return; } @@ -184,6 +191,46 @@ private static Collection distributeWay(Collection ways, return cmds; } + /** + * Test if single node oriented algorithm applies to the selection. + * @param nodes The selected node. Collection type and naming kept for compatibility with similar methods. + * @return true in this case + */ + private static boolean checkDistributeNode(List nodes) { + if (nodes.size() == 1) { + Node node = nodes.get(0); + int goodWays = 0; + for (Way way : node.getParentWays()) { + // the algorithm is applicable only if there is one way which: + // - is open and the selected node is a middle node, or + // - is closed and has at least 4 nodes (as 3 doesn't make sense and error-prone) + if (!way.isFirstLastNode(node) || (way.isClosed() && way.getRealNodesCount() > 3)) + goodWays++; + } + return goodWays == 1; + } + return false; + } + + /** + * Distribute a single node relative to way neighbours. + * @see DistributeAction#distributeNodes(Collection) + * @param nodes a single node in a collection to distribute + * @return Commands to execute to perform action + */ + private static Collection distributeNode(List nodes) { + final Node nodeToDistribute = nodes.get(0); + Way parent = nodeToDistribute.getParentWays().get(0); + + List neighbours = new ArrayList<>(parent.getNeighbours(nodeToDistribute)); + + // insert in the middle + neighbours.add(1, nodeToDistribute); + + // call the distribution method with 3 nodes + return distributeNodes(neighbours); + } + /** * Test if nodes oriented algorithm applies to the selection. * @param ways Selected ways @@ -197,7 +244,7 @@ private static boolean checkDistributeNodes(Collection ways, Collection ways, Collection distributeNodes(Collection nodes) { // Find from the selected nodes two that are the furthest apart. // Let's call them A and B. - double distance = Double.NEGATIVE_INFINITY; - - Node nodea = null; - Node nodeb = null; - - Collection itnodes = new LinkedList<>(nodes); - for (Node n : nodes) { - itnodes.remove(n); - for (Node m : itnodes) { - double dist = Math.sqrt(n.getEastNorth().distance(m.getEastNorth())); - if (dist > distance) { - nodea = n; - nodeb = m; - distance = dist; - } - } - } + Node[] furthestApart = nodeFurthestApart(new ArrayList<>(nodes)); + Node nodeA = furthestApart[0]; + Node nodeB = furthestApart[1]; - if (nodea == null || nodeb == null) { + if (nodeA == null || nodeB == null) { throw new IllegalArgumentException(); } // Remove the nodes A and B from the list of nodes to move - nodes.remove(nodea); - nodes.remove(nodeb); + nodes.remove(nodeA); + nodes.remove(nodeB); // Find out co-ords of A and B - double ax = nodea.getEastNorth().east(); - double ay = nodea.getEastNorth().north(); - double bx = nodeb.getEastNorth().east(); - double by = nodeb.getEastNorth().north(); + double ax = nodeA.getEastNorth().east(); + double ay = nodeA.getEastNorth().north(); + double bx = nodeB.getEastNorth().east(); + double by = nodeB.getEastNorth().north(); // A list of commands to do Collection cmds = new LinkedList<>(); - // Amount of nodes between A and B plus 1 + // Number of nodes between A and B plus 1 int num = nodes.size()+1; // Current number of node int pos = 0; while (!nodes.isEmpty()) { pos++; - Node s = null; // Find the node that is furthest from B (i.e. closest to A) - distance = Double.NEGATIVE_INFINITY; - for (Node n : nodes) { - double dist = Math.sqrt(nodeb.getEastNorth().distance(n.getEastNorth())); - if (dist > distance) { - s = n; - distance = dist; - } - } + Node s = getFurthestPrimitive(nodeB, nodes); if (s != null) { // First move the node to A's position, then move it towards B diff --git a/src/org/openstreetmap/josm/tools/Geometry.java b/src/org/openstreetmap/josm/tools/Geometry.java index 98cd6c5a34f..868ad929d2d 100644 --- a/src/org/openstreetmap/josm/tools/Geometry.java +++ b/src/org/openstreetmap/josm/tools/Geometry.java @@ -14,6 +14,7 @@ import java.util.Collection; import java.util.Collections; import java.util.Comparator; +import java.util.HashSet; import java.util.Iterator; import java.util.LinkedHashSet; import java.util.List; @@ -85,10 +86,10 @@ public enum PolygonIntersection { public static final double INTERSECTION_EPS_EAST_NORTH = 1e-4; /** - * Will find all intersection and add nodes there for list of given ways. + * Will find all intersections and add nodes there for list of given ways. * Handles self-intersections too. * And makes commands to add the intersection points to ways. - * + *

* Prerequisite: no two nodes have the same coordinates. * * @param ways a list of ways to test @@ -243,7 +244,7 @@ public static Set addIntersections(List ways, boolean test, List * (Imagine the path is continued beyond the endpoints, so you get two rays * starting from lineP2 and going through lineP1 and lineP3 respectively * which divide the plane into two parts. The test returns true, if testPoint @@ -531,7 +532,7 @@ public static EastNorth closestPointToLine(EastNorth lineP1, EastNorth lineP2, E /** * This method tests if secondNode is clockwise to first node. - * + *

* The line through the two points commonNode and firstNode divides the * plane into two parts. The test returns true, if secondNode lies in * the part that is to the right when traveling in the direction from @@ -841,11 +842,11 @@ public static Double computeArea(IPrimitive osm) { /** * Determines whether a way is oriented clockwise. - * + *

* Internals: Assuming a closed non-looping way, compute twice the area * of the polygon using the formula {@code 2 * area = sum (X[n] * Y[n+1] - X[n+1] * Y[n])}. - * If the area is negative the way is ordered in a clockwise direction. - * + * If the area is negative, the way is ordered in a clockwise direction. + *

* See http://paulbourke.net/geometry/polyarea/ * * @param w the way to be checked. @@ -993,10 +994,10 @@ public static EastNorth getCentroidEN(List nodes) { /** * Compute the center of the circle closest to different nodes. - * - * Ensure exact center computation in case nodes are already aligned in circle. + *

+ * Ensure exact center computations in case nodes are already aligned in circle. * This is done by least square method. - * Let be a_i x + b_i y + c_i = 0 equations of bisectors of each edges. + * Let be a_i x + b_i y + c_i = 0 equations of bisectors of each edge. * Center must be intersection of all bisectors. *

      *          [ a1  b1  ]         [ -c1 ]
@@ -1012,12 +1013,12 @@ public static EastNorth getCentroidEN(List nodes) {
     public static EastNorth getCenter(List nodes) {
         int nc = nodes.size();
         if (nc < 3) return null;
-        /**
-         * Equation of each bisector ax + by + c = 0
-         */
+
+        // Equation of each bisector ax + by + c = 0
         double[] a = new double[nc];
         double[] b = new double[nc];
         double[] c = new double[nc];
+
         // Compute equation of bisector
         for (int i = 0; i < nc; i++) {
             EastNorth pt1 = nodes.get(i).getEastNorth();
@@ -1281,8 +1282,8 @@ public double getPerimeter() {
     }
 
     /**
-     * Calculate area and perimeter length of a polygon.
-     *
+     * Calculate the area and perimeter length of a polygon.
+     * 

* Uses current projection; units are that of the projected coordinates. * * @param nodes the list of nodes representing the polygon @@ -1293,7 +1294,7 @@ public static AreaAndPerimeter getAreaAndPerimeter(List nodes } /** - * Calculate area and perimeter length of a polygon in the given projection. + * Calculate the area and perimeter length of a polygon in the given projection. * * @param nodes the list of nodes representing the polygon * @param projection the projection to use for the calculation, {@code null} defaults to {@link ProjectionRegistry#getProjection()} @@ -1325,10 +1326,10 @@ public static AreaAndPerimeter getAreaAndPerimeter(List nodes /** * Get the closest primitive to {@code osm} from the collection of - * OsmPrimitive {@code primitives} - * + * OsmPrimitive {@code primitives}. + *

* The {@code primitives} should be fully downloaded to ensure accuracy. - * + *

* Note: The complexity of this method is O(n*m), where n is the number of * children {@code osm} has plus 1, m is the number of children the * collection of primitives have plus the number of primitives in the @@ -1349,10 +1350,10 @@ public static T getClosestPrimitive(OsmPrimitive osm, C /** * Get the closest primitives to {@code osm} from the collection of - * OsmPrimitive {@code primitives} - * + * OsmPrimitive {@code primitives}. + *

* The {@code primitives} should be fully downloaded to ensure accuracy. - * + *

* Note: The complexity of this method is O(n*m), where n is the number of * children {@code osm} has plus 1, m is the number of children the * collection of primitives have plus the number of primitives in the @@ -1384,13 +1385,13 @@ public static Collection getClosestPrimitives(OsmPri /** * Get the furthest primitive to {@code osm} from the collection of - * OsmPrimitive {@code primitives} - * + * OsmPrimitive {@code primitives}. + *

* The {@code primitives} should be fully downloaded to ensure accuracy. - * + *

* It does NOT give the furthest primitive based off of the furthest * part of that primitive - * + *

* Note: The complexity of this method is O(n*m), where n is the number of * children {@code osm} has plus 1, m is the number of children the * collection of primitives have plus the number of primitives in the @@ -1410,13 +1411,13 @@ public static T getFurthestPrimitive(OsmPrimitive osm, /** * Get the furthest primitives to {@code osm} from the collection of - * OsmPrimitive {@code primitives} - * + * OsmPrimitive {@code primitives}. + *

* The {@code primitives} should be fully downloaded to ensure accuracy. - * + *

* It does NOT give the furthest primitive based off of the furthest * part of that primitive - * + *

* Note: The complexity of this method is O(n*m), where n is the number of * children {@code osm} has plus 1, m is the number of children the * collection of primitives have plus the number of primitives in the @@ -1431,8 +1432,9 @@ public static T getFurthestPrimitive(OsmPrimitive osm, public static Collection getFurthestPrimitives(OsmPrimitive osm, Collection primitives) { double furthestDistance = Double.NEGATIVE_INFINITY; TreeSet furthest = new TreeSet<>(); + double distance; for (T primitive : primitives) { - double distance = getDistance(osm, primitive); + distance = getDistance(osm, primitive); if (Double.isNaN(distance)) continue; int comp = Double.compare(distance, furthestDistance); if (comp > 0) { @@ -1453,7 +1455,7 @@ public static Collection getFurthestPrimitives(OsmPr * @return The distance between the primitives in meters * (or the unit of the current projection, see {@link Projection}). * May return {@link Double#NaN} if one of the primitives is incomplete. - * + *

* Note: The complexity is O(n*m), where (n,m) are the number of child * objects the {@link OsmPrimitive}s have. * @since 15035 @@ -1646,11 +1648,96 @@ public static ILatLon getLatLonFrom(final ILatLon original, final double angle, } /** - * Calculate closest distance between a line segment s1-s2 and a point p + * Return two nodes making up the line along which provided nodes must be aligned. + * + * @param nodes Nodes to be aligned. + * @return An array of two nodes. + * @throws IllegalArgumentException if nodes parameter is empty + */ + public static Node[] nodePairFurthestApart(List nodes) { + // Detect if selected nodes are on the same way. + + // Get ways passing though all selected nodes. + Set waysRef = null; + for (Node n: nodes) { + Collection ref = n.getParentWays(); + if (waysRef == null) + waysRef = new HashSet<>(ref); + else + waysRef.retainAll(ref); + } + + if (waysRef == null) { + throw new IllegalArgumentException(); + } + + // Nodes belong to multiple ways, return the most distant nodes. + if (waysRef.size() != 1) + return nodeFurthestApart(nodes); + + // All nodes are part of the same way. See #9605. + Way way = waysRef.iterator().next(); + + if (way.isClosed()) { + // Align these nodes on the line passing through the most distant nodes. + return nodeFurthestApart(nodes); + } + + Node nodea = null; + Node nodeb = null; + + // The way is open, align nodes on the line passing through the extremity nodes (most distant in the way + // sequence). See #9605#comment:3. + Set remainNodes = new HashSet<>(nodes); + for (Node n : way.getNodes()) { + if (!remainNodes.contains(n)) + continue; + if (nodea == null) + nodea = n; + if (remainNodes.size() == 1) { + nodeb = remainNodes.iterator().next(); + break; + } + remainNodes.remove(n); + } + + return new Node[] {nodea, nodeb}; + } + + /** + * Return the two nodes the most distant from the provided list. + * + * @param nodes List of nodes to analyze. + * @return An array containing the two most distant nodes. + */ + public static Node[] nodeFurthestApart(List nodes) { + Node node1 = null, node2 = null; + double minSqDistance = Double.NEGATIVE_INFINITY; + int nb; + + nb = nodes.size(); + for (int i = 0; i < nb - 1; i++) { + Node n = nodes.get(i); + for (int j = i + 1; j < nb; j++) { + Node m = nodes.get(j); + double sqDist = n.getEastNorth().distanceSq(m.getEastNorth()); + if (sqDist > minSqDistance) { + node1 = n; + node2 = m; + minSqDistance = sqDist; + } + } + } + + return new Node[] {node1, node2}; + } + + /** + * Calculate the closest distance between a line segment s1-s2 and a point p * @param s1 start of segment * @param s2 end of segment * @param p the point - * @return the square of the euclidean distance from p to the closest point on the segment + * @return the square of the Euclidean distance from p to the closest point on the segment */ private static double getSegmentNodeDistSq(EastNorth s1, EastNorth s2, EastNorth p) { EastNorth c1 = closestPointTo(s1, s2, p, true); diff --git a/test/unit/org/openstreetmap/josm/actions/DistributeActionTest.java b/test/unit/org/openstreetmap/josm/actions/DistributeActionTest.java new file mode 100644 index 00000000000..4ed6554e0b1 --- /dev/null +++ b/test/unit/org/openstreetmap/josm/actions/DistributeActionTest.java @@ -0,0 +1,198 @@ +package org.openstreetmap.josm.actions; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.openstreetmap.josm.data.coor.LatLon; +import org.openstreetmap.josm.data.osm.DataSet; +import org.openstreetmap.josm.data.osm.Node; +import org.openstreetmap.josm.data.osm.Way; +import org.openstreetmap.josm.gui.MainApplication; +import org.openstreetmap.josm.gui.layer.OsmDataLayer; +import org.openstreetmap.josm.testutils.annotations.Main; +import org.openstreetmap.josm.testutils.annotations.Projection; + +import java.util.Random; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +/** + * Unit tests for class {@link DistributeAction}. + */ +@Main +@Projection +class DistributeActionTest { + + private static final Random random = new Random(); + private DataSet ds = new DataSet(); + + @BeforeEach + void setUp() { + ds = new DataSet(); + } + + @Test + void testNoAlignment() { + Node n = new Node(LatLon.ZERO); + ds.addPrimitive(n); + + OsmDataLayer layer = new OsmDataLayer(ds, "", null); + MainApplication.getLayerManager().addLayer(layer); + assertNotNull(MainApplication.getLayerManager().getActiveLayer()); + + // select a single node + layer.getDataSet().setSelected(n.getPrimitiveId()); + assertEquals(1, layer.getDataSet().getSelected().size()); + + new DistributeAction().actionPerformed(null); + assertEquals(n.lat(), LatLon.ZERO.lat()); + assertEquals(n.lon(), LatLon.ZERO.lon()); + } + + @Test + void testWholeWayAlignment() { + Way way = new Way(); + final int totalNodeCount = 120; // should be at least three + final int innerNodeCount = totalNodeCount - 2; + final int lonLimit = 10; + + // add first node + Node n = new Node(new LatLon(LatLon.ZERO)); + ds.addPrimitive(n); + way.addNode(n); + + // add interim nodes + for (int i = 0; i < innerNodeCount; i++) { + // 0.001 subtracted to avoid cases where the random double == lonLimit + n = new Node(new LatLon(0, getRandomDoubleInRange(0, lonLimit - 0.001))); + ds.addPrimitive(n); + way.addNode(n); + } + + // add last node + n = new Node(new LatLon(0, lonLimit)); + ds.addPrimitive(n); + way.addNode(n); + ds.addPrimitive(way); + + + OsmDataLayer layer = new OsmDataLayer(ds, "", null); + MainApplication.getLayerManager().addLayer(layer); + assertNotNull(MainApplication.getLayerManager().getActiveLayer()); + + // select the way + layer.getDataSet().setSelected(way.getPrimitiveId()); + assertEquals(1, layer.getDataSet().getSelected().size()); + + new DistributeAction().actionPerformed(null); + + for (int i = 0; i < totalNodeCount; i++) { + double evenGapNthNode = ((double) lonLimit / (totalNodeCount - 1)) * i; + assertEquals(evenGapNthNode, way.getNode(i).lon(), 1e-7); + } + } + + @Test + void testNodesAlignment() { + Way way = new Way(); + final int totalNodeCount = 1100; // should be at least three + final int innerNodeCount = totalNodeCount - 2; + final int lonLimit = 10; + + // add first node + Node n = new Node(new LatLon(LatLon.ZERO)); + ds.addPrimitive(n); + way.addNode(n); + + // add interim nodes + for (int i = 0; i < innerNodeCount; i++) { + // 0.001 subtracted to avoid cases where the random double == lonLimit + n = new Node(new LatLon(0, getRandomDoubleInRange(0, lonLimit - 0.001))); + ds.addPrimitive(n); + way.addNode(n); + } + + // add last node + n = new Node(new LatLon(0, lonLimit)); + ds.addPrimitive(n); + way.addNode(n); + ds.addPrimitive(way); + + + OsmDataLayer layer = new OsmDataLayer(ds, "", null); + MainApplication.getLayerManager().addLayer(layer); + assertNotNull(MainApplication.getLayerManager().getActiveLayer()); + + // select all nodes on the way + layer.getDataSet().setSelected(way.getNodes()); + assertEquals(way.getNodes().size(), layer.getDataSet().getSelected().size()); + + new DistributeAction().actionPerformed(null); + + // FIXME: the assertion will most likely fail due to the current core implementation: + // the two *furthest nodes* are selected as alignment base, then they evenly ordered along a virtual way. + // Test expectation: the *end nodes* of a virtual way are distribution basis. + for (int i = 0; i < totalNodeCount; i++) { + double evenGapNthNode = ((double) lonLimit / (totalNodeCount - 1)) * i; + assertEquals(evenGapNthNode, way.getNode(i).lon(), 1e-7); + } + } + + @Test + void testSingleNodeAlignment() { + Way way = new Way(); + final int totalNodeCount = 11; // should be in range [3,180]! + final int lonLimit = totalNodeCount - 1; + + Node n; + Node selectedNode = null; + + // add nodes except the last one + for (int i = 0; i < totalNodeCount - 1; i++) { + if (i == 1) { + n = new Node(new LatLon(0, 0.1)); + selectedNode = n; + } else { + n = new Node(new LatLon(0, i)); + } + ds.addPrimitive(n); + way.addNode(n); + } + + // add last node + n = new Node(new LatLon(0, lonLimit)); + ds.addPrimitive(n); + way.addNode(n); + ds.addPrimitive(way); + + + OsmDataLayer layer = new OsmDataLayer(ds, "", null); + MainApplication.getLayerManager().addLayer(layer); + assertNotNull(MainApplication.getLayerManager().getActiveLayer()); + + // select a single node + layer.getDataSet().setSelected(selectedNode.getPrimitiveId()); + assertEquals(1, layer.getDataSet().getSelected().size()); + + new DistributeAction().actionPerformed(null); + + for (int i = 0; i < totalNodeCount; i++) { + double evenGapNthNode = ((double) lonLimit / (totalNodeCount - 1)) * i; + assertEquals(evenGapNthNode, way.getNode(i).lon(), 1e-7); + } + } + + + /** + * @param min range lower value + * @param max range upper value + * @return a single double in the given range + * @throws IllegalArgumentException if min {@code >=} max + */ + private static double getRandomDoubleInRange(double min, double max) { + if (min >= max) { + throw new IllegalArgumentException("Invalid range. Max must be greater than min."); + } + return min + (max - min) * random.nextDouble(); + } +} \ No newline at end of file