// License: GPL. For details, see LICENSE file. package org.openstreetmap.josm.actions; import static org.openstreetmap.josm.gui.help.HelpUtil.ht; 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.Arrays; import java.util.Collection; import java.util.Comparator; import java.util.LinkedList; import java.util.List; import javax.swing.JOptionPane; import org.openstreetmap.josm.Main; import org.openstreetmap.josm.command.AddCommand; import org.openstreetmap.josm.command.ChangeCommand; import org.openstreetmap.josm.command.Command; import org.openstreetmap.josm.command.SequenceCommand; import org.openstreetmap.josm.data.coor.EastNorth; import org.openstreetmap.josm.data.coor.LatLon; import org.openstreetmap.josm.data.osm.Node; import org.openstreetmap.josm.data.osm.OsmPrimitive; import org.openstreetmap.josm.data.osm.Way; import org.openstreetmap.josm.gui.Notification; import org.openstreetmap.josm.tools.Geometry; import org.openstreetmap.josm.tools.Shortcut; /** * - Create a new circle from two selected nodes or a way with 2 nodes which represent the diameter of the circle. * - Create a new circle from three selected nodes--or a way with 3 nodes. * - Useful for roundabouts * * Notes: * * If a way is selected, it is changed. If nodes are selected a new way is created. * So if you've got a way with nodes it makes a difference between running this on the way or the nodes! * * The existing nodes are retained, and additional nodes are inserted regularly * to achieve the desired number of nodes (`createcircle.nodecount`). * BTW: Someone might want to implement projection corrections for this... * * @since 996 * * @author Henry Loenwind * @author Sebastian Masch * @author Alain Delplanque */ public final class CreateCircleAction extends JosmAction { /** * Constructs a new {@code CreateCircleAction}. */ public CreateCircleAction() { super(tr("Create Circle"), "aligncircle", tr("Create a circle from three selected nodes."), Shortcut.registerShortcut("tools:createcircle", tr("Tool: {0}", tr("Create Circle")), KeyEvent.VK_O, Shortcut.SHIFT), true, "createcircle", true); putValue("help", ht("/Action/CreateCircle")); } /** * Distributes nodes according to the algorithm of election with largest remainder. * @param angles Array of PolarNode ordered by increasing angles * @param nodesCount Number of nodes to be distributed * @return Array of number of nodes to put in each arc */ private int[] distributeNodes(PolarNode[] angles, int nodesCount) { int[] count = new int[angles.length]; double[] width = new double[angles.length]; double[] remainder = new double[angles.length]; for(int i = 0; i < angles.length; i++) { width[i] = angles[(i+1) % angles.length].a - angles[i].a; if(width[i] < 0) width[i] += 2*Math.PI; } int assign = 0; for(int i = 0; i < angles.length; i++) { double part = width[i] / 2.0 / Math.PI * nodesCount; count[i] = (int) Math.floor(part); remainder[i] = part - count[i]; assign += count[i]; } while(assign < nodesCount) { int imax = 0; for(int i = 1; i < angles.length; i++) if(remainder[i] > remainder[imax]) imax = i; count[imax]++; remainder[imax] = 0; assign++; } return count; } /** * Class designed to create a couple between a node and its angle relative to the center of the circle. */ private static class PolarNode { double a; Node node; PolarNode(EastNorth center, Node n) { EastNorth pt = n.getEastNorth(); this.a = Math.atan2(pt.north() - center.north(), pt.east() - center.east()); this.node = n; } } /** * Comparator used to order PolarNode relative to their angle. */ private static class PolarNodeComparator implements Comparator { @Override public int compare(PolarNode pc1, PolarNode pc2) { if(pc1.a < pc2.a) return -1; else if(pc1.a == pc2.a) return 0; else return 1; } } @Override public void actionPerformed(ActionEvent e) { if (!isEnabled()) return; int numberOfNodesInCircle = Main.pref.getInteger("createcircle.nodecount", 16); if (numberOfNodesInCircle < 1) { numberOfNodesInCircle = 1; } else if (numberOfNodesInCircle > 100) { numberOfNodesInCircle = 100; } Collection sel = getCurrentDataSet().getSelected(); List nodes = new LinkedList<>(); Way existingWay = null; for (OsmPrimitive osm : sel) if (osm instanceof Node) { nodes.add((Node)osm); } // special case if no single nodes are selected and exactly one way is: // then use the way's nodes if (nodes.isEmpty() && (sel.size() == 1)) { for (OsmPrimitive osm : sel) if (osm instanceof Way) { existingWay = ((Way)osm); for (Node n : ((Way)osm).getNodes()) { if(!nodes.contains(n)) { nodes.add(n); } } } } if (nodes.size() < 2 || nodes.size() > 3) { new Notification( tr("Please select exactly two or three nodes or one way with exactly two or three nodes.")) .setIcon(JOptionPane.INFORMATION_MESSAGE) .setDuration(Notification.TIME_LONG) .show(); return; } // now we can start doing things to OSM data Collection cmds = new LinkedList<>(); EastNorth center = null; if (nodes.size() == 2) { // diameter: two single nodes needed or a way with two nodes Node n1 = nodes.get(0); double x1 = n1.getEastNorth().east(); double y1 = n1.getEastNorth().north(); Node n2 = nodes.get(1); double x2 = n2.getEastNorth().east(); double y2 = n2.getEastNorth().north(); // calculate the center (xc/yc) double xc = 0.5 * (x1 + x2); double yc = 0.5 * (y1 + y2); center = new EastNorth(xc, yc); } else { // triangle: three single nodes needed or a way with three nodes center = Geometry.getCenter(nodes); if (center == null) { notifyNodesNotOnCircle(); return; } } // calculate the radius (r) EastNorth n1 = nodes.get(0).getEastNorth(); double r = Math.sqrt(Math.pow(center.east()-n1.east(),2) + Math.pow(center.north()-n1.north(),2)); // Order nodes by angle PolarNode[] angles = new PolarNode[nodes.size()]; for(int i = 0; i < nodes.size(); i++) { angles[i] = new PolarNode(center, nodes.get(i)); } Arrays.sort(angles, new PolarNodeComparator()); int[] count = distributeNodes(angles, numberOfNodesInCircle >= nodes.size() ? numberOfNodesInCircle - nodes.size() : 0); // build a way for the circle List wayToAdd = new ArrayList<>(); for(int i = 0; i < nodes.size(); i++) { wayToAdd.add(angles[i].node); double delta = angles[(i+1) % nodes.size()].a - angles[i].a; if(delta < 0) delta += 2*Math.PI; for(int j = 0; j < count[i]; j++) { double alpha = angles[i].a + (j+1)*delta/(count[i]+1); double x = center.east() + r*Math.cos(alpha); double y = center.north() + r*Math.sin(alpha); LatLon ll = Main.getProjection().eastNorth2latlon(new EastNorth(x,y)); if (ll.isOutSideWorld()) { notifyNodesNotOnCircle(); return; } Node n = new Node(ll); wayToAdd.add(n); cmds.add(new AddCommand(n)); } } wayToAdd.add(wayToAdd.get(0)); // close the circle if (existingWay == null) { Way newWay = new Way(); newWay.setNodes(wayToAdd); cmds.add(new AddCommand(newWay)); } else { Way newWay = new Way(existingWay); newWay.setNodes(wayToAdd); cmds.add(new ChangeCommand(existingWay, newWay)); } Main.main.undoRedo.add(new SequenceCommand(tr("Create Circle"), cmds)); Main.map.repaint(); } private static void notifyNodesNotOnCircle() { new Notification( tr("Those nodes are not in a circle. Aborting.")) .setIcon(JOptionPane.WARNING_MESSAGE) .show(); } @Override protected void updateEnabledState() { if (getCurrentDataSet() == null) { setEnabled(false); } else { updateEnabledState(getCurrentDataSet().getSelected()); } } @Override protected void updateEnabledState(Collection selection) { setEnabled(selection != null && !selection.isEmpty()); } }