001 // License: GPL. Copyright 2007 by Immanuel Scholz and others 002 package org.openstreetmap.josm.actions; 003 004 import static org.openstreetmap.josm.gui.help.HelpUtil.ht; 005 import static org.openstreetmap.josm.tools.I18n.tr; 006 007 import java.awt.event.ActionEvent; 008 import java.awt.event.KeyEvent; 009 import java.util.ArrayList; 010 import java.util.Collection; 011 import java.util.LinkedList; 012 import java.util.List; 013 014 import javax.swing.JOptionPane; 015 016 import org.openstreetmap.josm.Main; 017 import org.openstreetmap.josm.command.AddCommand; 018 import org.openstreetmap.josm.command.ChangeCommand; 019 import org.openstreetmap.josm.command.Command; 020 import org.openstreetmap.josm.command.DeleteCommand; 021 import org.openstreetmap.josm.command.SequenceCommand; 022 import org.openstreetmap.josm.data.coor.EastNorth; 023 import org.openstreetmap.josm.data.osm.Node; 024 import org.openstreetmap.josm.data.osm.OsmPrimitive; 025 import org.openstreetmap.josm.data.osm.Way; 026 import org.openstreetmap.josm.tools.Shortcut; 027 028 /** 029 * - Create a new circle from two selected nodes or a way with 2 nodes which represent the diameter of the circle. 030 * - Create a new circle from three selected nodes--or a way with 3 nodes. 031 * - Useful for roundabouts 032 * 033 * Note: If a way is selected, it is changed. If nodes are selected a new way is created. 034 * So if you've got a way with nodes it makes a difference between running this on the way or the nodes! 035 * 036 * BTW: Someone might want to implement projection corrections for this... 037 * 038 * @author Henry Loenwind, based on much copy&Paste from other Actions. 039 * @author Sebastian Masch 040 */ 041 public final class CreateCircleAction extends JosmAction { 042 043 public CreateCircleAction() { 044 super(tr("Create Circle"), "createcircle", tr("Create a circle from three selected nodes."), 045 Shortcut.registerShortcut("tools:createcircle", tr("Tool: {0}", tr("Create Circle")), 046 KeyEvent.VK_O, Shortcut.SHIFT), true); 047 putValue("help", ht("/Action/CreateCircle")); 048 } 049 050 private double calcang(double xc, double yc, double x, double y) { 051 // calculate the angle from xc|yc to x|y 052 if (xc == x && yc == y) 053 return 0; // actually invalid, but we won't have this case in this context 054 double yd = Math.abs(y - yc); 055 if (yd == 0 && xc < x) 056 return 0; 057 if (yd == 0 && xc > x) 058 return Math.PI; 059 double xd = Math.abs(x - xc); 060 double a = Math.atan2(xd, yd); 061 if (y > yc) { 062 a = Math.PI - a; 063 } 064 if (x < xc) { 065 a = -a; 066 } 067 a = 1.5*Math.PI + a; 068 if (a < 0) { 069 a += 2*Math.PI; 070 } 071 if (a >= 2*Math.PI) { 072 a -= 2*Math.PI; 073 } 074 return a; 075 } 076 077 public void actionPerformed(ActionEvent e) { 078 if (!isEnabled()) 079 return; 080 081 int numberOfNodesInCircle = Main.pref.getInteger("createcircle.nodecount", 8); 082 if (numberOfNodesInCircle < 1) { 083 numberOfNodesInCircle = 1; 084 } else if (numberOfNodesInCircle > 100) { 085 numberOfNodesInCircle = 100; 086 } 087 088 Collection<OsmPrimitive> sel = getCurrentDataSet().getSelected(); 089 List<Node> nodes = new LinkedList<Node>(); 090 Way existingWay = null; 091 092 for (OsmPrimitive osm : sel) 093 if (osm instanceof Node) { 094 nodes.add((Node)osm); 095 } 096 097 // special case if no single nodes are selected and exactly one way is: 098 // then use the way's nodes 099 if ((nodes.size() == 0) && (sel.size() == 1)) { 100 for (OsmPrimitive osm : sel) 101 if (osm instanceof Way) { 102 existingWay = ((Way)osm); 103 for (Node n : ((Way)osm).getNodes()) 104 { 105 if(!nodes.contains(n)) { 106 nodes.add(n); 107 } 108 } 109 } 110 } 111 112 // now we can start doing things to OSM data 113 Collection<Command> cmds = new LinkedList<Command>(); 114 115 if (nodes.size() == 2) { 116 // diameter: two single nodes needed or a way with two nodes 117 118 Node n1 = nodes.get(0); 119 double x1 = n1.getEastNorth().east(); 120 double y1 = n1.getEastNorth().north(); 121 Node n2 = nodes.get(1); 122 double x2 = n2.getEastNorth().east(); 123 double y2 = n2.getEastNorth().north(); 124 125 // calculate the center (xc/yc) 126 double xc = 0.5 * (x1 + x2); 127 double yc = 0.5 * (y1 + y2); 128 129 // calculate the radius (r) 130 double r = Math.sqrt(Math.pow(xc-x1,2) + Math.pow(yc-y1,2)); 131 132 // find where to put the existing nodes 133 double a1 = calcang(xc, yc, x1, y1); 134 double a2 = calcang(xc, yc, x2, y2); 135 if (a1 < a2) { double at = a1; Node nt = n1; a1 = a2; n1 = n2; a2 = at; n2 = nt; } 136 137 // build a way for the circle 138 List<Node> wayToAdd = new ArrayList<Node>(); 139 140 for (int i = 1; i <= numberOfNodesInCircle; i++) { 141 double a = a2 + 2*Math.PI*(1.0 - i/(double)numberOfNodesInCircle); // "1-" to get it clock-wise 142 143 // insert existing nodes if they fit before this new node (999 means "already added this node") 144 if ((a1 < 999) && (a1 > a - 1E-9) && (a1 < a + 1E-9)) { 145 wayToAdd.add(n1); 146 a1 = 999; 147 } 148 else if ((a2 < 999) && (a2 > a - 1E-9) && (a2 < a + 1E-9)) { 149 wayToAdd.add(n2); 150 a2 = 999; 151 } 152 else { 153 // get the position of the new node and insert it 154 double x = xc + r*Math.cos(a); 155 double y = yc + r*Math.sin(a); 156 Node n = new Node(Main.getProjection().eastNorth2latlon(new EastNorth(x,y))); 157 wayToAdd.add(n); 158 cmds.add(new AddCommand(n)); 159 } 160 } 161 wayToAdd.add(wayToAdd.get(0)); // close the circle 162 if (existingWay == null) { 163 Way newWay = new Way(); 164 newWay.setNodes(wayToAdd); 165 cmds.add(new AddCommand(newWay)); 166 } else { 167 Way newWay = new Way(existingWay); 168 newWay.setNodes(wayToAdd); 169 cmds.add(new ChangeCommand(existingWay, newWay)); 170 } 171 172 // the first node may be unused/abandoned if createcircle.nodecount is odd 173 if (a1 < 999) { 174 // if it is, delete it 175 List<OsmPrimitive> parents = n1.getReferrers(); 176 if (parents.isEmpty() || ((parents.size() == 1) && (parents.contains(existingWay)))) { 177 cmds.add(new DeleteCommand(n1)); 178 } 179 180 // or insert it 181 // wayToAdd.nodes.add((numberOfNodesInCircle - 1) / 2, n1); 182 } 183 184 } else if (nodes.size() == 3) { 185 // triangle: three single nodes needed or a way with three nodes 186 187 // let's get some shorter names 188 Node n1 = nodes.get(0); 189 double x1 = n1.getEastNorth().east(); 190 double y1 = n1.getEastNorth().north(); 191 Node n2 = nodes.get(1); 192 double x2 = n2.getEastNorth().east(); 193 double y2 = n2.getEastNorth().north(); 194 Node n3 = nodes.get(2); 195 double x3 = n3.getEastNorth().east(); 196 double y3 = n3.getEastNorth().north(); 197 198 // calculate the center (xc/yc) 199 double s = 0.5*((x2 - x3)*(x1 - x3) - (y2 - y3)*(y3 - y1)); 200 double sUnder = (x1 - x2)*(y3 - y1) - (y2 - y1)*(x1 - x3); 201 202 if (sUnder == 0) { 203 JOptionPane.showMessageDialog( 204 Main.parent, 205 tr("Those nodes are not in a circle. Aborting."), 206 tr("Warning"), 207 JOptionPane.WARNING_MESSAGE 208 ); 209 return; 210 } 211 212 s /= sUnder; 213 214 double xc = 0.5*(x1 + x2) + s*(y2 - y1); 215 double yc = 0.5*(y1 + y2) + s*(x1 - x2); 216 217 // calculate the radius (r) 218 double r = Math.sqrt(Math.pow(xc-x1,2) + Math.pow(yc-y1,2)); 219 220 // find where to put the existing nodes 221 double a1 = calcang(xc, yc, x1, y1); 222 double a2 = calcang(xc, yc, x2, y2); 223 double a3 = calcang(xc, yc, x3, y3); 224 if (a1 < a2) { double at = a1; Node nt = n1; a1 = a2; n1 = n2; a2 = at; n2 = nt; } 225 if (a2 < a3) { double at = a2; Node nt = n2; a2 = a3; n2 = n3; a3 = at; n3 = nt; } 226 if (a1 < a2) { double at = a1; Node nt = n1; a1 = a2; n1 = n2; a2 = at; n2 = nt; } 227 228 // build a way for the circle 229 List<Node> wayToAdd = new ArrayList<Node>(); 230 for (int i = 1; i <= numberOfNodesInCircle; i++) { 231 double a = 2*Math.PI*(1.0 - i/(double)numberOfNodesInCircle); // "1-" to get it clock-wise 232 // insert existing nodes if they fit before this new node (999 means "already added this node") 233 if (a1 < 999 && a1 > a) { 234 wayToAdd.add(n1); 235 a1 = 999; 236 } 237 if (a2 < 999 && a2 > a) { 238 wayToAdd.add(n2); 239 a2 = 999; 240 } 241 if (a3 < 999 && a3 > a) { 242 wayToAdd.add(n3); 243 a3 = 999; 244 } 245 // get the position of the new node and insert it 246 double x = xc + r*Math.cos(a); 247 double y = yc + r*Math.sin(a); 248 Node n = new Node(Main.getProjection().eastNorth2latlon(new EastNorth(x,y))); 249 wayToAdd.add(n); 250 cmds.add(new AddCommand(n)); 251 } 252 wayToAdd.add(wayToAdd.get(0)); // close the circle 253 if (existingWay == null) { 254 Way newWay = new Way(); 255 newWay.setNodes(wayToAdd); 256 cmds.add(new AddCommand(newWay)); 257 } else { 258 Way newWay = new Way(existingWay); 259 newWay.setNodes(wayToAdd); 260 cmds.add(new ChangeCommand(existingWay, newWay)); 261 } 262 263 } else { 264 JOptionPane.showMessageDialog( 265 Main.parent, 266 tr("Please select exactly two or three nodes or one way with exactly two or three nodes."), 267 tr("Information"), 268 JOptionPane.INFORMATION_MESSAGE 269 ); 270 return; 271 } 272 273 Main.main.undoRedo.add(new SequenceCommand(tr("Create Circle"), cmds)); 274 Main.map.repaint(); 275 } 276 277 @Override 278 protected void updateEnabledState() { 279 if (getCurrentDataSet() == null) { 280 setEnabled(false); 281 } else { 282 updateEnabledState(getCurrentDataSet().getSelected()); 283 } 284 } 285 286 @Override 287 protected void updateEnabledState(Collection<? extends OsmPrimitive> selection) { 288 setEnabled(selection != null && !selection.isEmpty()); 289 } 290 }