001//License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.actions; 003 004import static org.openstreetmap.josm.gui.help.HelpUtil.ht; 005import static org.openstreetmap.josm.tools.I18n.tr; 006 007import java.awt.event.ActionEvent; 008import java.awt.event.KeyEvent; 009import java.math.BigDecimal; 010import java.math.MathContext; 011import java.util.Collection; 012import java.util.HashSet; 013import java.util.LinkedList; 014import java.util.List; 015import java.util.Set; 016 017import javax.swing.JOptionPane; 018 019import org.openstreetmap.josm.Main; 020import org.openstreetmap.josm.command.Command; 021import org.openstreetmap.josm.command.MoveCommand; 022import org.openstreetmap.josm.command.SequenceCommand; 023import org.openstreetmap.josm.data.coor.EastNorth; 024import org.openstreetmap.josm.data.osm.Node; 025import org.openstreetmap.josm.data.osm.OsmPrimitive; 026import org.openstreetmap.josm.data.osm.Way; 027import org.openstreetmap.josm.gui.Notification; 028import org.openstreetmap.josm.tools.Geometry; 029import org.openstreetmap.josm.tools.Shortcut; 030 031/** 032 * Aligns all selected nodes within a circle. (Useful for roundabouts) 033 * 034 * @author Matthew Newton 035 * @author Petr DlouhĂ˝ 036 * @author Teemu Koskinen 037 */ 038public final class AlignInCircleAction extends JosmAction { 039 040 /** 041 * Constructs a new {@code AlignInCircleAction}. 042 */ 043 public AlignInCircleAction() { 044 super(tr("Align Nodes in Circle"), "aligncircle", tr("Move the selected nodes into a circle."), 045 Shortcut.registerShortcut("tools:aligncircle", tr("Tool: {0}", tr("Align Nodes in Circle")), 046 KeyEvent.VK_O, Shortcut.DIRECT), true); 047 putValue("help", ht("/Action/AlignInCircle")); 048 } 049 050 public double distance(EastNorth n, EastNorth m) { 051 double easd, nord; 052 easd = n.east() - m.east(); 053 nord = n.north() - m.north(); 054 return Math.sqrt(easd * easd + nord * nord); 055 } 056 057 public class PolarCoor { 058 double radius; 059 double angle; 060 EastNorth origin = new EastNorth(0, 0); 061 double azimuth = 0; 062 063 PolarCoor(double radius, double angle) { 064 this(radius, angle, new EastNorth(0, 0), 0); 065 } 066 067 PolarCoor(double radius, double angle, EastNorth origin, double azimuth) { 068 this.radius = radius; 069 this.angle = angle; 070 this.origin = origin; 071 this.azimuth = azimuth; 072 } 073 074 PolarCoor(EastNorth en) { 075 this(en, new EastNorth(0, 0), 0); 076 } 077 078 PolarCoor(EastNorth en, EastNorth origin, double azimuth) { 079 radius = distance(en, origin); 080 angle = Math.atan2(en.north() - origin.north(), en.east() - origin.east()); 081 this.origin = origin; 082 this.azimuth = azimuth; 083 } 084 085 public EastNorth toEastNorth() { 086 return new EastNorth(radius * Math.cos(angle - azimuth) + origin.east(), radius * Math.sin(angle - azimuth) 087 + origin.north()); 088 } 089 } 090 091 @Override 092 public void actionPerformed(ActionEvent e) { 093 if (!isEnabled()) 094 return; 095 096 Collection<OsmPrimitive> sel = getCurrentDataSet().getSelected(); 097 List<Node> nodes = new LinkedList<Node>(); 098 List<Way> ways = new LinkedList<Way>(); 099 EastNorth center = null; 100 double radius = 0; 101 boolean regular = false; 102 103 for (OsmPrimitive osm : sel) { 104 if (osm instanceof Node) { 105 nodes.add((Node) osm); 106 } else if (osm instanceof Way) { 107 ways.add((Way) osm); 108 } 109 } 110 111 // special case if no single nodes are selected and exactly one way is: 112 // then use the way's nodes 113 if ((nodes.size() <= 2) && (ways.size() == 1)) { 114 Way way = ways.get(0); 115 116 // some more special combinations: 117 // When is selected node that is part of the way, then make a regular polygon, selected 118 // node doesn't move. 119 // I haven't got better idea, how to activate that function. 120 // 121 // When one way and one node is selected, set center to position of that node. 122 // When one more node, part of the way, is selected, set the radius equal to the 123 // distance between two nodes. 124 if (nodes.size() > 0) { 125 if (nodes.size() == 1 && way.containsNode(nodes.get(0)) && allowRegularPolygon(way.getNodes())) { 126 regular = true; 127 } else if (nodes.size() >= 2) { 128 center = nodes.get(way.containsNode(nodes.get(0)) ? 1 : 0).getEastNorth(); 129 if (nodes.size() == 2) { 130 radius = distance(nodes.get(0).getEastNorth(), nodes.get(1).getEastNorth()); 131 } 132 } 133 nodes.clear(); 134 } 135 136 for (Node n : way.getNodes()) { 137 if (!nodes.contains(n)) { 138 nodes.add(n); 139 } 140 } 141 } 142 143 if (nodes.size() < 4) { 144 new Notification( 145 tr("Please select at least four nodes.")) 146 .setIcon(JOptionPane.INFORMATION_MESSAGE) 147 .setDuration(Notification.TIME_SHORT) 148 .show(); 149 return; 150 } 151 152 // Reorder the nodes if they didn't come from a single way 153 if (ways.size() != 1) { 154 // First calculate the average point 155 156 BigDecimal east = BigDecimal.ZERO; 157 BigDecimal north = BigDecimal.ZERO; 158 159 for (Node n : nodes) { 160 BigDecimal x = new BigDecimal(n.getEastNorth().east()); 161 BigDecimal y = new BigDecimal(n.getEastNorth().north()); 162 east = east.add(x, MathContext.DECIMAL128); 163 north = north.add(y, MathContext.DECIMAL128); 164 } 165 BigDecimal nodesSize = new BigDecimal(nodes.size()); 166 east = east.divide(nodesSize, MathContext.DECIMAL128); 167 north = north.divide(nodesSize, MathContext.DECIMAL128); 168 169 EastNorth average = new EastNorth(east.doubleValue(), north.doubleValue()); 170 List<Node> newNodes = new LinkedList<Node>(); 171 172 // Then reorder them based on heading from the average point 173 while (!nodes.isEmpty()) { 174 double maxHeading = -1.0; 175 Node maxNode = null; 176 for (Node n : nodes) { 177 double heading = average.heading(n.getEastNorth()); 178 if (heading > maxHeading) { 179 maxHeading = heading; 180 maxNode = n; 181 } 182 } 183 newNodes.add(maxNode); 184 nodes.remove(maxNode); 185 } 186 187 nodes = newNodes; 188 } 189 190 if (center == null) { 191 // Compute the centroid of nodes 192 center = Geometry.getCentroid(nodes); 193 } 194 // Node "center" now is central to all selected nodes. 195 196 // Now calculate the average distance to each node from the 197 // centre. This method is ok as long as distances are short 198 // relative to the distance from the N or S poles. 199 if (radius == 0) { 200 for (Node n : nodes) { 201 radius += distance(center, n.getEastNorth()); 202 } 203 radius = radius / nodes.size(); 204 } 205 206 Collection<Command> cmds = new LinkedList<Command>(); 207 208 PolarCoor pc; 209 210 if (regular) { // Make a regular polygon 211 double angle = Math.PI * 2 / nodes.size(); 212 pc = new PolarCoor(nodes.get(0).getEastNorth(), center, 0); 213 214 if (pc.angle > (new PolarCoor(nodes.get(1).getEastNorth(), center, 0).angle)) { 215 angle *= -1; 216 } 217 218 pc.radius = radius; 219 for (Node n : nodes) { 220 EastNorth no = pc.toEastNorth(); 221 cmds.add(new MoveCommand(n, no.east() - n.getEastNorth().east(), no.north() - n.getEastNorth().north())); 222 pc.angle += angle; 223 } 224 } else { // Move each node to that distance from the centre. 225 for (Node n : nodes) { 226 pc = new PolarCoor(n.getEastNorth(), center, 0); 227 pc.radius = radius; 228 EastNorth no = pc.toEastNorth(); 229 cmds.add(new MoveCommand(n, no.east() - n.getEastNorth().east(), no.north() - n.getEastNorth().north())); 230 } 231 } 232 233 Main.main.undoRedo.add(new SequenceCommand(tr("Align Nodes in Circle"), cmds)); 234 Main.map.repaint(); 235 } 236 237 @Override 238 protected void updateEnabledState() { 239 setEnabled(getCurrentDataSet() != null && !getCurrentDataSet().getSelected().isEmpty()); 240 } 241 242 @Override 243 protected void updateEnabledState(Collection<? extends OsmPrimitive> selection) { 244 setEnabled(selection != null && !selection.isEmpty()); 245 } 246 247 /** 248 * Determines if a regular polygon is allowed to be created with the given nodes collection. 249 * @param nodes The nodes collection to check. 250 * @return true if all nodes in the given collection are referred by the same object, and no other one (see #8431) 251 */ 252 protected boolean allowRegularPolygon(Collection<Node> nodes) { 253 Set<OsmPrimitive> allReferrers = new HashSet<OsmPrimitive>(); 254 for (Node n : nodes) { 255 List<OsmPrimitive> referrers = n.getReferrers(); 256 if (referrers.size() > 1 || (allReferrers.addAll(referrers) && allReferrers.size() > 1)) { 257 return false; 258 } 259 } 260 return true; 261 } 262}