/** * Perform AlignInCircle action. * * <p>A fixed node is a node for which it is forbidden to change the angle relative to center of * the circle. All other nodes are uniformly distributed. * * <p>Case 1: One unclosed way. --> allow action, and align selected way nodes If nodes * contained by this way are selected, there are fix. If nodes outside from the way are selected * there are ignored. * * <p>Case 2: One or more ways are selected and can be joined into a polygon --> allow action, * and align selected ways nodes If 1 node outside of way is selected, it became center If 1 node * outside and 1 node inside are selected there define center and radius If no outside node and 2 * inside nodes are selected those 2 nodes define diameter In all other cases outside nodes are * ignored In all cases, selected nodes are fix, nodes with more than one referrers are fix (first * referrer is the selected way) * * <p>Case 3: Only nodes are selected --> Align these nodes, all are fix */ @Override public void actionPerformed(ActionEvent e) { if (!isEnabled()) return; Collection<OsmPrimitive> sel = getCurrentDataSet().getSelected(); List<Node> nodes = new LinkedList<>(); // fixNodes: All nodes for which the angle relative to center should not be modified Set<Node> fixNodes = new HashSet<>(); List<Way> ways = new LinkedList<>(); EastNorth center = null; double radius = 0; for (OsmPrimitive osm : sel) { if (osm instanceof Node) { nodes.add((Node) osm); } else if (osm instanceof Way) { ways.add((Way) osm); } } if (ways.size() == 1 && !ways.get(0).isClosed()) { // Case 1 Way w = ways.get(0); fixNodes.add(w.firstNode()); fixNodes.add(w.lastNode()); fixNodes.addAll(nodes); fixNodes.addAll(collectNodesWithExternReferers(ways)); // Temporary closed way used to reorder nodes Way closedWay = new Way(w); closedWay.addNode(w.firstNode()); List<Way> usedWays = new ArrayList<>(1); usedWays.add(closedWay); nodes = collectNodesAnticlockwise(usedWays); } else if (!ways.isEmpty() && checkWaysArePolygon(ways)) { // Case 2 List<Node> inside = new ArrayList<>(); List<Node> outside = new ArrayList<>(); for (Node n : nodes) { boolean isInside = false; for (Way w : ways) { if (w.getNodes().contains(n)) { isInside = true; break; } } if (isInside) inside.add(n); else outside.add(n); } if (outside.size() == 1 && inside.isEmpty()) { center = outside.get(0).getEastNorth(); } else if (outside.size() == 1 && inside.size() == 1) { center = outside.get(0).getEastNorth(); radius = distance(center, inside.get(0).getEastNorth()); } else if (inside.size() == 2 && outside.isEmpty()) { // 2 nodes inside, define diameter EastNorth en0 = inside.get(0).getEastNorth(); EastNorth en1 = inside.get(1).getEastNorth(); center = new EastNorth((en0.east() + en1.east()) / 2, (en0.north() + en1.north()) / 2); radius = distance(en0, en1) / 2; } fixNodes.addAll(inside); fixNodes.addAll(collectNodesWithExternReferers(ways)); nodes = collectNodesAnticlockwise(ways); if (nodes.size() < 4) { new Notification(tr("Not enough nodes in selected ways.")) .setIcon(JOptionPane.INFORMATION_MESSAGE) .setDuration(Notification.TIME_SHORT) .show(); return; } } else if (ways.isEmpty() && nodes.size() > 3) { // Case 3 fixNodes.addAll(nodes); // No need to reorder nodes since all are fix } else { // Invalid action new Notification(tr("Please select at least four nodes.")) .setIcon(JOptionPane.INFORMATION_MESSAGE) .setDuration(Notification.TIME_SHORT) .show(); return; } if (center == null) { // Compute the center of nodes center = Geometry.getCenter(nodes); if (center == null) { new Notification(tr("Cannot determine center of selected nodes.")) .setIcon(JOptionPane.INFORMATION_MESSAGE) .setDuration(Notification.TIME_SHORT) .show(); return; } } // Now calculate the average distance to each node from the // center. This method is ok as long as distances are short // relative to the distance from the N or S poles. if (radius == 0) { for (Node n : nodes) { radius += distance(center, n.getEastNorth()); } radius = radius / nodes.size(); } if (!actionAllowed(nodes)) return; Collection<Command> cmds = new LinkedList<>(); // Move each node to that distance from the center. // Nodes that are not "fix" will be adjust making regular arcs. int nodeCount = nodes.size(); // Search first fixed node int startPosition = 0; for (startPosition = 0; startPosition < nodeCount; startPosition++) { if (fixNodes.contains(nodes.get(startPosition % nodeCount))) break; } int i = startPosition; // Start position for current arc int j; // End position for current arc while (i < startPosition + nodeCount) { for (j = i + 1; j < startPosition + nodeCount; j++) { if (fixNodes.contains(nodes.get(j % nodeCount))) break; } Node first = nodes.get(i % nodeCount); PolarCoor pcFirst = new PolarCoor(first.getEastNorth(), center, 0); pcFirst.radius = radius; cmds.add(pcFirst.createMoveCommand(first)); if (j > i + 1) { double delta; if (j == i + nodeCount) { delta = 2 * Math.PI / nodeCount; } else { PolarCoor pcLast = new PolarCoor(nodes.get(j % nodeCount).getEastNorth(), center, 0); delta = pcLast.angle - pcFirst.angle; if (delta < 0) // Assume each PolarCoor.angle is in range ]-pi; pi] delta += 2 * Math.PI; delta /= j - i; } for (int k = i + 1; k < j; k++) { PolarCoor p = new PolarCoor(radius, pcFirst.angle + (k - i) * delta, center, 0); cmds.add(p.createMoveCommand(nodes.get(k % nodeCount))); } } i = j; // Update start point for next iteration } Main.main.undoRedo.add(new SequenceCommand(tr("Align Nodes in Circle"), cmds)); Main.map.repaint(); }