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