| 1 | // License: GPL. Copyright 2007 by Immanuel Scholz and others |
|---|
| 2 | package org.openstreetmap.josm.actions.mapmode; |
|---|
| 3 | |
|---|
| 4 | import static org.openstreetmap.josm.gui.help.HelpUtil.ht; |
|---|
| 5 | import static org.openstreetmap.josm.tools.I18n.tr; |
|---|
| 6 | |
|---|
| 7 | import java.awt.AWTEvent; |
|---|
| 8 | import java.awt.BasicStroke; |
|---|
| 9 | import java.awt.Color; |
|---|
| 10 | import java.awt.Cursor; |
|---|
| 11 | import java.awt.Graphics2D; |
|---|
| 12 | import java.awt.Point; |
|---|
| 13 | import java.awt.Rectangle; |
|---|
| 14 | import java.awt.Toolkit; |
|---|
| 15 | import java.awt.event.AWTEventListener; |
|---|
| 16 | import java.awt.event.ActionEvent; |
|---|
| 17 | import java.awt.event.InputEvent; |
|---|
| 18 | import java.awt.event.KeyEvent; |
|---|
| 19 | import java.awt.event.MouseEvent; |
|---|
| 20 | import java.awt.geom.AffineTransform; |
|---|
| 21 | import java.awt.geom.GeneralPath; |
|---|
| 22 | import java.awt.geom.Line2D; |
|---|
| 23 | import java.awt.geom.NoninvertibleTransformException; |
|---|
| 24 | import java.awt.geom.Point2D; |
|---|
| 25 | import java.util.ArrayList; |
|---|
| 26 | import java.util.Collection; |
|---|
| 27 | import java.util.LinkedList; |
|---|
| 28 | import java.util.List; |
|---|
| 29 | |
|---|
| 30 | import org.openstreetmap.josm.Main; |
|---|
| 31 | import org.openstreetmap.josm.command.AddCommand; |
|---|
| 32 | import org.openstreetmap.josm.command.ChangeCommand; |
|---|
| 33 | import org.openstreetmap.josm.command.Command; |
|---|
| 34 | import org.openstreetmap.josm.command.MoveCommand; |
|---|
| 35 | import org.openstreetmap.josm.command.SequenceCommand; |
|---|
| 36 | import org.openstreetmap.josm.data.Bounds; |
|---|
| 37 | import org.openstreetmap.josm.data.coor.EastNorth; |
|---|
| 38 | import org.openstreetmap.josm.data.osm.Node; |
|---|
| 39 | import org.openstreetmap.josm.data.osm.OsmPrimitive; |
|---|
| 40 | import org.openstreetmap.josm.data.osm.Way; |
|---|
| 41 | import org.openstreetmap.josm.data.osm.WaySegment; |
|---|
| 42 | import org.openstreetmap.josm.data.osm.visitor.paint.PaintColors; |
|---|
| 43 | import org.openstreetmap.josm.gui.MapFrame; |
|---|
| 44 | import org.openstreetmap.josm.gui.MapView; |
|---|
| 45 | import org.openstreetmap.josm.gui.layer.Layer; |
|---|
| 46 | import org.openstreetmap.josm.gui.layer.MapViewPaintable; |
|---|
| 47 | import org.openstreetmap.josm.gui.layer.OsmDataLayer; |
|---|
| 48 | import org.openstreetmap.josm.tools.Geometry; |
|---|
| 49 | import org.openstreetmap.josm.tools.ImageProvider; |
|---|
| 50 | import org.openstreetmap.josm.tools.Shortcut; |
|---|
| 51 | |
|---|
| 52 | /** |
|---|
| 53 | * Makes a rectangle from a line, or modifies a rectangle. |
|---|
| 54 | */ |
|---|
| 55 | public class ExtrudeAction extends MapMode implements MapViewPaintable { |
|---|
| 56 | |
|---|
| 57 | enum Mode { extrude, translate, select, create_new } |
|---|
| 58 | |
|---|
| 59 | private Mode mode = Mode.select; |
|---|
| 60 | |
|---|
| 61 | /** |
|---|
| 62 | * If true, when extruding create new node even if segments parallel. |
|---|
| 63 | */ |
|---|
| 64 | private boolean alwaysCreateNodes = false; |
|---|
| 65 | private long mouseDownTime = 0; |
|---|
| 66 | private WaySegment selectedSegment = null; |
|---|
| 67 | private Color selectedColor; |
|---|
| 68 | |
|---|
| 69 | /** |
|---|
| 70 | * Possible directions to move to. |
|---|
| 71 | */ |
|---|
| 72 | private List<EastNorth> possibleMoveDirections; |
|---|
| 73 | |
|---|
| 74 | /** |
|---|
| 75 | * The direction that is currently active. |
|---|
| 76 | */ |
|---|
| 77 | private EastNorth activeMoveDirection; |
|---|
| 78 | |
|---|
| 79 | /** |
|---|
| 80 | * The position of the mouse cursor when the drag action was initiated. |
|---|
| 81 | */ |
|---|
| 82 | private Point initialMousePos; |
|---|
| 83 | /** |
|---|
| 84 | * The time which needs to pass between click and release before something |
|---|
| 85 | * counts as a move, in milliseconds |
|---|
| 86 | */ |
|---|
| 87 | private int initialMoveDelay = 200; |
|---|
| 88 | /** |
|---|
| 89 | * The initial EastNorths of node1 and node2 |
|---|
| 90 | */ |
|---|
| 91 | private EastNorth initialN1en; |
|---|
| 92 | private EastNorth initialN2en; |
|---|
| 93 | /** |
|---|
| 94 | * The new EastNorths of node1 and node2 |
|---|
| 95 | */ |
|---|
| 96 | private EastNorth newN1en; |
|---|
| 97 | private EastNorth newN2en; |
|---|
| 98 | |
|---|
| 99 | /** |
|---|
| 100 | * the command that performed last move. |
|---|
| 101 | */ |
|---|
| 102 | private MoveCommand moveCommand; |
|---|
| 103 | |
|---|
| 104 | /** The cursor for the 'create_new' mode. */ |
|---|
| 105 | private final Cursor cursorCreateNew; |
|---|
| 106 | |
|---|
| 107 | /** |
|---|
| 108 | * This listener is used to indicate the 'create_new' mode, if the Alt modifier is pressed. |
|---|
| 109 | */ |
|---|
| 110 | private final AWTEventListener altKeyListener = new AWTEventListener() { |
|---|
| 111 | @Override |
|---|
| 112 | public void eventDispatched(AWTEvent e) { |
|---|
| 113 | if(Main.map == null || Main.map.mapView == null || !Main.map.mapView.isActiveLayerDrawable()) |
|---|
| 114 | return; |
|---|
| 115 | InputEvent ie = (InputEvent) e; |
|---|
| 116 | boolean alt = (ie.getModifiers() & (ActionEvent.ALT_MASK|InputEvent.ALT_GRAPH_MASK)) != 0; |
|---|
| 117 | if(mode == Mode.select) { |
|---|
| 118 | Main.map.mapView.setNewCursor(alt ? cursorCreateNew : cursor, this); |
|---|
| 119 | } |
|---|
| 120 | } |
|---|
| 121 | }; |
|---|
| 122 | |
|---|
| 123 | /** |
|---|
| 124 | * Create a new SelectAction |
|---|
| 125 | * @param mapFrame The MapFrame this action belongs to. |
|---|
| 126 | */ |
|---|
| 127 | public ExtrudeAction(MapFrame mapFrame) { |
|---|
| 128 | super(tr("Extrude"), "extrude/extrude", tr("Create areas"), |
|---|
| 129 | Shortcut.registerShortcut("mapmode:extrude", tr("Mode: {0}", tr("Extrude")), KeyEvent.VK_X, Shortcut.DIRECT), |
|---|
| 130 | mapFrame, |
|---|
| 131 | ImageProvider.getCursor("normal", "rectangle")); |
|---|
| 132 | putValue("help", ht("/Action/Extrude")); |
|---|
| 133 | initialMoveDelay = Main.pref.getInteger("edit.initial-move-delay",200); |
|---|
| 134 | selectedColor = PaintColors.SELECTED.get(); |
|---|
| 135 | cursorCreateNew = ImageProvider.getCursor("normal", "rectangle_plus"); |
|---|
| 136 | } |
|---|
| 137 | |
|---|
| 138 | @Override public String getModeHelpText() { |
|---|
| 139 | if (mode == Mode.translate) |
|---|
| 140 | return tr("Move a segment along its normal, then release the mouse button."); |
|---|
| 141 | else if (mode == Mode.extrude) |
|---|
| 142 | return tr("Draw a rectangle of the desired size, then release the mouse button."); |
|---|
| 143 | else if (mode == Mode.create_new) |
|---|
| 144 | return tr("Draw a rectangle of the desired size, then release the mouse button."); |
|---|
| 145 | else |
|---|
| 146 | return tr("Drag a way segment to make a rectangle. Ctrl-drag to move a segment along its normal, " + |
|---|
| 147 | "Alt-drag to create a new rectangle, double click to add a new node."); |
|---|
| 148 | } |
|---|
| 149 | |
|---|
| 150 | @Override public boolean layerIsSupported(Layer l) { |
|---|
| 151 | return l instanceof OsmDataLayer; |
|---|
| 152 | } |
|---|
| 153 | |
|---|
| 154 | @Override public void enterMode() { |
|---|
| 155 | super.enterMode(); |
|---|
| 156 | Main.map.mapView.addMouseListener(this); |
|---|
| 157 | Main.map.mapView.addMouseMotionListener(this); |
|---|
| 158 | try { |
|---|
| 159 | Toolkit.getDefaultToolkit().addAWTEventListener(altKeyListener, AWTEvent.KEY_EVENT_MASK); |
|---|
| 160 | } catch (SecurityException ex) { |
|---|
| 161 | } |
|---|
| 162 | } |
|---|
| 163 | |
|---|
| 164 | @Override public void exitMode() { |
|---|
| 165 | Main.map.mapView.removeMouseListener(this); |
|---|
| 166 | Main.map.mapView.removeMouseMotionListener(this); |
|---|
| 167 | Main.map.mapView.removeTemporaryLayer(this); |
|---|
| 168 | try { |
|---|
| 169 | Toolkit.getDefaultToolkit().removeAWTEventListener(altKeyListener); |
|---|
| 170 | } catch (SecurityException ex) { |
|---|
| 171 | } |
|---|
| 172 | super.exitMode(); |
|---|
| 173 | } |
|---|
| 174 | |
|---|
| 175 | /** |
|---|
| 176 | * If the left mouse button is pressed over a segment, switch |
|---|
| 177 | * to either extrude, translate or create_new mode depending on whether Ctrl or Alt is held. |
|---|
| 178 | */ |
|---|
| 179 | @Override public void mousePressed(MouseEvent e) { |
|---|
| 180 | if(!Main.map.mapView.isActiveLayerVisible()) |
|---|
| 181 | return; |
|---|
| 182 | if (!(Boolean)this.getValue("active")) |
|---|
| 183 | return; |
|---|
| 184 | if (e.getButton() != MouseEvent.BUTTON1) |
|---|
| 185 | return; |
|---|
| 186 | |
|---|
| 187 | updateKeyModifiers(e); |
|---|
| 188 | |
|---|
| 189 | selectedSegment = Main.map.mapView.getNearestWaySegment(e.getPoint(), OsmPrimitive.isSelectablePredicate); |
|---|
| 190 | |
|---|
| 191 | if (selectedSegment == null) { |
|---|
| 192 | // If nothing gets caught, stay in select mode |
|---|
| 193 | } else { |
|---|
| 194 | // Otherwise switch to another mode |
|---|
| 195 | |
|---|
| 196 | if (ctrl) { |
|---|
| 197 | mode = Mode.translate; |
|---|
| 198 | } else if (alt) { |
|---|
| 199 | mode = Mode.create_new; |
|---|
| 200 | // create a new segment and then select and extrude the new segment |
|---|
| 201 | getCurrentDataSet().setSelected(selectedSegment.way); |
|---|
| 202 | alwaysCreateNodes = true; |
|---|
| 203 | } else { |
|---|
| 204 | mode = Mode.extrude; |
|---|
| 205 | getCurrentDataSet().setSelected(selectedSegment.way); |
|---|
| 206 | alwaysCreateNodes = shift; |
|---|
| 207 | } |
|---|
| 208 | |
|---|
| 209 | // remember initial positions for segment nodes. |
|---|
| 210 | initialN1en = selectedSegment.getFirstNode().getEastNorth(); |
|---|
| 211 | initialN2en = selectedSegment.getSecondNode().getEastNorth(); |
|---|
| 212 | |
|---|
| 213 | //gather possible move directions - perpendicular to the selected segment and parallel to neighbor segments |
|---|
| 214 | possibleMoveDirections = new ArrayList<EastNorth>(); |
|---|
| 215 | possibleMoveDirections.add(new EastNorth( |
|---|
| 216 | initialN1en.getY() - initialN2en.getY(), |
|---|
| 217 | initialN2en.getX() - initialN1en.getX())); |
|---|
| 218 | |
|---|
| 219 | //add directions parallel to neighbor segments |
|---|
| 220 | |
|---|
| 221 | Node prevNode = getPreviousNode(selectedSegment.lowerIndex); |
|---|
| 222 | if (prevNode != null) { |
|---|
| 223 | EastNorth en = prevNode.getEastNorth(); |
|---|
| 224 | possibleMoveDirections.add(new EastNorth( |
|---|
| 225 | initialN1en.getX() - en.getX(), |
|---|
| 226 | initialN1en.getY() - en.getY())); |
|---|
| 227 | } |
|---|
| 228 | |
|---|
| 229 | Node nextNode = getNextNode(selectedSegment.lowerIndex + 1); |
|---|
| 230 | if (nextNode != null) { |
|---|
| 231 | EastNorth en = nextNode.getEastNorth(); |
|---|
| 232 | possibleMoveDirections.add(new EastNorth( |
|---|
| 233 | initialN2en.getX() - en.getX(), |
|---|
| 234 | initialN2en.getY() - en.getY())); |
|---|
| 235 | } |
|---|
| 236 | |
|---|
| 237 | // Signifies that nothing has happened yet |
|---|
| 238 | newN1en = null; |
|---|
| 239 | newN2en = null; |
|---|
| 240 | moveCommand = null; |
|---|
| 241 | |
|---|
| 242 | Main.map.mapView.addTemporaryLayer(this); |
|---|
| 243 | |
|---|
| 244 | updateStatusLine(); |
|---|
| 245 | Main.map.mapView.repaint(); |
|---|
| 246 | |
|---|
| 247 | // Make note of time pressed |
|---|
| 248 | mouseDownTime = System.currentTimeMillis(); |
|---|
| 249 | |
|---|
| 250 | // Make note of mouse position |
|---|
| 251 | initialMousePos = e.getPoint(); |
|---|
| 252 | } |
|---|
| 253 | } |
|---|
| 254 | |
|---|
| 255 | /** |
|---|
| 256 | * Perform action depending on what mode we're in. |
|---|
| 257 | */ |
|---|
| 258 | @Override public void mouseDragged(MouseEvent e) { |
|---|
| 259 | if(!Main.map.mapView.isActiveLayerVisible()) |
|---|
| 260 | return; |
|---|
| 261 | |
|---|
| 262 | // do not count anything as a drag if it lasts less than 100 milliseconds. |
|---|
| 263 | if (System.currentTimeMillis() - mouseDownTime < initialMoveDelay) |
|---|
| 264 | return; |
|---|
| 265 | |
|---|
| 266 | if (mode == Mode.select) { |
|---|
| 267 | // Just sit tight and wait for mouse to be released. |
|---|
| 268 | } else { |
|---|
| 269 | //move, create new and extrude mode - move the selected segment |
|---|
| 270 | |
|---|
| 271 | EastNorth initialMouseEn = Main.map.mapView.getEastNorth(initialMousePos.x, initialMousePos.y); |
|---|
| 272 | EastNorth mouseEn = Main.map.mapView.getEastNorth(e.getPoint().x, e.getPoint().y); |
|---|
| 273 | EastNorth mouseMovement = new EastNorth(mouseEn.getX() - initialMouseEn.getX(), mouseEn.getY() - initialMouseEn.getY()); |
|---|
| 274 | |
|---|
| 275 | double bestDistance = Double.POSITIVE_INFINITY; |
|---|
| 276 | EastNorth bestMovement = null; |
|---|
| 277 | activeMoveDirection = null; |
|---|
| 278 | |
|---|
| 279 | //find the best movement direction and vector |
|---|
| 280 | for (EastNorth direction: possibleMoveDirections) { |
|---|
| 281 | EastNorth movement = calculateSegmentOffset(initialN1en, initialN2en, direction , mouseEn); |
|---|
| 282 | if (movement == null) { |
|---|
| 283 | //if direction parallel to segment. |
|---|
| 284 | continue; |
|---|
| 285 | } |
|---|
| 286 | |
|---|
| 287 | double distanceFromMouseMovement = movement.distance(mouseMovement); |
|---|
| 288 | if (bestDistance > distanceFromMouseMovement) { |
|---|
| 289 | bestDistance = distanceFromMouseMovement; |
|---|
| 290 | activeMoveDirection = direction; |
|---|
| 291 | bestMovement = movement; |
|---|
| 292 | } |
|---|
| 293 | } |
|---|
| 294 | |
|---|
| 295 | newN1en = new EastNorth(initialN1en.getX() + bestMovement.getX(), initialN1en.getY() + bestMovement.getY()); |
|---|
| 296 | newN2en = new EastNorth(initialN2en.getX() + bestMovement.getX(), initialN2en.getY() + bestMovement.getY()); |
|---|
| 297 | |
|---|
| 298 | // find out the movement distance, in metres |
|---|
| 299 | double distance = Main.getProjection().eastNorth2latlon(initialN1en).greatCircleDistance(Main.getProjection().eastNorth2latlon(newN1en)); |
|---|
| 300 | Main.map.statusLine.setDist(distance); |
|---|
| 301 | updateStatusLine(); |
|---|
| 302 | |
|---|
| 303 | Main.map.mapView.setNewCursor(Cursor.MOVE_CURSOR, this); |
|---|
| 304 | |
|---|
| 305 | if (mode == Mode.extrude || mode == Mode.create_new) { |
|---|
| 306 | //nothing here |
|---|
| 307 | } else if (mode == Mode.translate) { |
|---|
| 308 | //move nodes to new position |
|---|
| 309 | if (moveCommand == null) { |
|---|
| 310 | //make a new move command |
|---|
| 311 | Collection<OsmPrimitive> nodelist = new LinkedList<OsmPrimitive>(); |
|---|
| 312 | nodelist.add(selectedSegment.getFirstNode()); |
|---|
| 313 | nodelist.add(selectedSegment.getSecondNode()); |
|---|
| 314 | moveCommand = new MoveCommand(nodelist, bestMovement.getX(), bestMovement.getY()); |
|---|
| 315 | Main.main.undoRedo.add(moveCommand); |
|---|
| 316 | } else { |
|---|
| 317 | //reuse existing move command |
|---|
| 318 | moveCommand.moveAgainTo(bestMovement.getX(), bestMovement.getY()); |
|---|
| 319 | } |
|---|
| 320 | } |
|---|
| 321 | |
|---|
| 322 | Main.map.mapView.repaint(); |
|---|
| 323 | } |
|---|
| 324 | } |
|---|
| 325 | |
|---|
| 326 | /** |
|---|
| 327 | * Do anything that needs to be done, then switch back to select mode |
|---|
| 328 | */ |
|---|
| 329 | @Override public void mouseReleased(MouseEvent e) { |
|---|
| 330 | |
|---|
| 331 | if(!Main.map.mapView.isActiveLayerVisible()) |
|---|
| 332 | return; |
|---|
| 333 | |
|---|
| 334 | if (mode == Mode.select) { |
|---|
| 335 | // Nothing to be done |
|---|
| 336 | } else { |
|---|
| 337 | if (mode == Mode.create_new) { |
|---|
| 338 | if (e.getPoint().distance(initialMousePos) > 10 && newN1en != null) { |
|---|
| 339 | // crete a new rectangle |
|---|
| 340 | Collection<Command> cmds = new LinkedList<Command>(); |
|---|
| 341 | Node third = new Node(newN2en); |
|---|
| 342 | Node fourth = new Node(newN1en); |
|---|
| 343 | Way wnew = new Way(); |
|---|
| 344 | wnew.addNode(selectedSegment.getFirstNode()); |
|---|
| 345 | wnew.addNode(selectedSegment.getSecondNode()); |
|---|
| 346 | wnew.addNode(third); |
|---|
| 347 | wnew.addNode(fourth); |
|---|
| 348 | // ... and close the way |
|---|
| 349 | wnew.addNode(selectedSegment.getFirstNode()); |
|---|
| 350 | // undo support |
|---|
| 351 | cmds.add(new AddCommand(third)); |
|---|
| 352 | cmds.add(new AddCommand(fourth)); |
|---|
| 353 | cmds.add(new AddCommand(wnew)); |
|---|
| 354 | Command c = new SequenceCommand(tr("Extrude Way"), cmds); |
|---|
| 355 | Main.main.undoRedo.add(c); |
|---|
| 356 | getCurrentDataSet().setSelected(wnew); |
|---|
| 357 | } |
|---|
| 358 | } else if (mode == Mode.extrude) { |
|---|
| 359 | if( e.getClickCount() == 2 && e.getPoint().equals(initialMousePos) ) { |
|---|
| 360 | // double click add a new node |
|---|
| 361 | // Should maybe do the same as in DrawAction and fetch all nearby segments? |
|---|
| 362 | WaySegment ws = Main.map.mapView.getNearestWaySegment(e.getPoint(), OsmPrimitive.isSelectablePredicate); |
|---|
| 363 | if (ws != null) { |
|---|
| 364 | Node n = new Node(Main.map.mapView.getLatLon(e.getX(), e.getY())); |
|---|
| 365 | EastNorth A = ws.getFirstNode().getEastNorth(); |
|---|
| 366 | EastNorth B = ws.getSecondNode().getEastNorth(); |
|---|
| 367 | n.setEastNorth(Geometry.closestPointToSegment(A, B, n.getEastNorth())); |
|---|
| 368 | Way wnew = new Way(ws.way); |
|---|
| 369 | wnew.addNode(ws.lowerIndex+1, n); |
|---|
| 370 | SequenceCommand cmds = new SequenceCommand(tr("Add a new node to an existing way"), |
|---|
| 371 | new AddCommand(n), new ChangeCommand(ws.way, wnew)); |
|---|
| 372 | Main.main.undoRedo.add(cmds); |
|---|
| 373 | } |
|---|
| 374 | } |
|---|
| 375 | else if (e.getPoint().distance(initialMousePos) > 10 && newN1en != null && selectedSegment != null) { |
|---|
| 376 | // create extrusion |
|---|
| 377 | |
|---|
| 378 | Collection<Command> cmds = new LinkedList<Command>(); |
|---|
| 379 | Way wnew = new Way(selectedSegment.way); |
|---|
| 380 | int insertionPoint = selectedSegment.lowerIndex + 1; |
|---|
| 381 | |
|---|
| 382 | //find if the new points overlap existing segments (in case of 90 degree angles) |
|---|
| 383 | Node prevNode = getPreviousNode(selectedSegment.lowerIndex); |
|---|
| 384 | boolean nodeOverlapsSegment = prevNode != null && Geometry.segmentsParallel(initialN1en, prevNode.getEastNorth(), initialN1en, newN1en); |
|---|
| 385 | boolean hasOtherWays = this.hasNodeOtherWays(selectedSegment.getFirstNode(), selectedSegment.way); |
|---|
| 386 | |
|---|
| 387 | if (nodeOverlapsSegment && !alwaysCreateNodes && !hasOtherWays) { |
|---|
| 388 | //move existing node |
|---|
| 389 | Node n1Old = selectedSegment.getFirstNode(); |
|---|
| 390 | cmds.add(new MoveCommand(n1Old, Main.getProjection().eastNorth2latlon(newN1en))); |
|---|
| 391 | } else { |
|---|
| 392 | //introduce new node |
|---|
| 393 | Node n1New = new Node(Main.getProjection().eastNorth2latlon(newN1en)); |
|---|
| 394 | wnew.addNode(insertionPoint, n1New); |
|---|
| 395 | insertionPoint ++; |
|---|
| 396 | cmds.add(new AddCommand(n1New)); |
|---|
| 397 | } |
|---|
| 398 | |
|---|
| 399 | //find if the new points overlap existing segments (in case of 90 degree angles) |
|---|
| 400 | Node nextNode = getNextNode(selectedSegment.lowerIndex + 1); |
|---|
| 401 | nodeOverlapsSegment = nextNode != null && Geometry.segmentsParallel(initialN2en, nextNode.getEastNorth(), initialN2en, newN2en); |
|---|
| 402 | hasOtherWays = hasNodeOtherWays(selectedSegment.getSecondNode(), selectedSegment.way); |
|---|
| 403 | |
|---|
| 404 | if (nodeOverlapsSegment && !alwaysCreateNodes && !hasOtherWays) { |
|---|
| 405 | //move existing node |
|---|
| 406 | Node n2Old = selectedSegment.getSecondNode(); |
|---|
| 407 | cmds.add(new MoveCommand(n2Old, Main.getProjection().eastNorth2latlon(newN2en))); |
|---|
| 408 | } else { |
|---|
| 409 | //introduce new node |
|---|
| 410 | Node n2New = new Node(Main.getProjection().eastNorth2latlon(newN2en)); |
|---|
| 411 | wnew.addNode(insertionPoint, n2New); |
|---|
| 412 | insertionPoint ++; |
|---|
| 413 | cmds.add(new AddCommand(n2New)); |
|---|
| 414 | } |
|---|
| 415 | |
|---|
| 416 | //the way was a single segment, close the way |
|---|
| 417 | if (wnew.getNodesCount() == 4) { |
|---|
| 418 | wnew.addNode(selectedSegment.getFirstNode()); |
|---|
| 419 | } |
|---|
| 420 | |
|---|
| 421 | cmds.add(new ChangeCommand(selectedSegment.way, wnew)); |
|---|
| 422 | Command c = new SequenceCommand(tr("Extrude Way"), cmds); |
|---|
| 423 | Main.main.undoRedo.add(c); |
|---|
| 424 | } |
|---|
| 425 | } else if (mode == Mode.translate) { |
|---|
| 426 | //Commit translate |
|---|
| 427 | //the move command is already committed in mouseDragged |
|---|
| 428 | } |
|---|
| 429 | |
|---|
| 430 | boolean alt = (e.getModifiers() & (ActionEvent.ALT_MASK|InputEvent.ALT_GRAPH_MASK)) != 0; |
|---|
| 431 | // Switch back into select mode |
|---|
| 432 | Main.map.mapView.setNewCursor(alt ? cursorCreateNew : cursor, this); |
|---|
| 433 | Main.map.mapView.removeTemporaryLayer(this); |
|---|
| 434 | selectedSegment = null; |
|---|
| 435 | moveCommand = null; |
|---|
| 436 | mode = Mode.select; |
|---|
| 437 | |
|---|
| 438 | updateStatusLine(); |
|---|
| 439 | Main.map.mapView.repaint(); |
|---|
| 440 | } |
|---|
| 441 | } |
|---|
| 442 | |
|---|
| 443 | /** |
|---|
| 444 | * This method tests if a node has other ways apart from the given one. |
|---|
| 445 | * @param node |
|---|
| 446 | * @param myWay |
|---|
| 447 | * @return true of node belongs only to myWay, false if there are more ways. |
|---|
| 448 | */ |
|---|
| 449 | private boolean hasNodeOtherWays(Node node, Way myWay) { |
|---|
| 450 | for (OsmPrimitive p : node.getReferrers()) { |
|---|
| 451 | if (p instanceof Way && p.isUsable() && p != myWay) |
|---|
| 452 | return true; |
|---|
| 453 | } |
|---|
| 454 | return false; |
|---|
| 455 | } |
|---|
| 456 | |
|---|
| 457 | /*** |
|---|
| 458 | * This method calculates offset amount by witch to move the given segment perpendicularly for it to be in line with mouse position. |
|---|
| 459 | * @param segmentP1 |
|---|
| 460 | * @param segmentP2 |
|---|
| 461 | * @param targetPos |
|---|
| 462 | * @return offset amount of P1 and P2. |
|---|
| 463 | */ |
|---|
| 464 | private static EastNorth calculateSegmentOffset(EastNorth segmentP1, EastNorth segmentP2, EastNorth moveDirection, |
|---|
| 465 | EastNorth targetPos) { |
|---|
| 466 | EastNorth intersectionPoint = Geometry.getLineLineIntersection(segmentP1, segmentP2, targetPos, |
|---|
| 467 | new EastNorth(targetPos.getX() + moveDirection.getX(), targetPos.getY() + moveDirection.getY())); |
|---|
| 468 | |
|---|
| 469 | if (intersectionPoint == null) |
|---|
| 470 | return null; |
|---|
| 471 | else |
|---|
| 472 | //return distance form base to target position |
|---|
| 473 | return new EastNorth(targetPos.getX() - intersectionPoint.getX(), |
|---|
| 474 | targetPos.getY() - intersectionPoint.getY()); |
|---|
| 475 | } |
|---|
| 476 | |
|---|
| 477 | |
|---|
| 478 | /** |
|---|
| 479 | * Gets a node from selected way before given index. |
|---|
| 480 | * @param index index of current node |
|---|
| 481 | * @return previous node or null if there are no nodes there. |
|---|
| 482 | */ |
|---|
| 483 | private Node getPreviousNode(int index) { |
|---|
| 484 | if (index > 0) |
|---|
| 485 | return selectedSegment.way.getNode(index - 1); |
|---|
| 486 | else if (selectedSegment.way.isClosed()) |
|---|
| 487 | return selectedSegment.way.getNode(selectedSegment.way.getNodesCount() - 2); |
|---|
| 488 | else |
|---|
| 489 | return null; |
|---|
| 490 | } |
|---|
| 491 | |
|---|
| 492 | /** |
|---|
| 493 | * Gets a node from selected way before given index. |
|---|
| 494 | * @param index index of current node |
|---|
| 495 | * @return next node or null if there are no nodes there. |
|---|
| 496 | */ |
|---|
| 497 | private Node getNextNode(int index) { |
|---|
| 498 | int count = selectedSegment.way.getNodesCount(); |
|---|
| 499 | if (index < count - 1) |
|---|
| 500 | return selectedSegment.way.getNode(index + 1); |
|---|
| 501 | else if (selectedSegment.way.isClosed()) |
|---|
| 502 | return selectedSegment.way.getNode(1); |
|---|
| 503 | else |
|---|
| 504 | return null; |
|---|
| 505 | } |
|---|
| 506 | |
|---|
| 507 | public void paint(Graphics2D g, MapView mv, Bounds box) { |
|---|
| 508 | if (mode == Mode.select) { |
|---|
| 509 | // Nothing to do |
|---|
| 510 | } else { |
|---|
| 511 | if (newN1en != null) { |
|---|
| 512 | Graphics2D g2 = g; |
|---|
| 513 | g2.setColor(selectedColor); |
|---|
| 514 | g2.setStroke(new BasicStroke(3, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND)); |
|---|
| 515 | |
|---|
| 516 | Point p1 = mv.getPoint(initialN1en); |
|---|
| 517 | Point p2 = mv.getPoint(initialN2en); |
|---|
| 518 | Point p3 = mv.getPoint(newN1en); |
|---|
| 519 | Point p4 = mv.getPoint(newN2en); |
|---|
| 520 | |
|---|
| 521 | if (mode == Mode.extrude || mode == Mode.create_new) { |
|---|
| 522 | // Draw rectangle around new area. |
|---|
| 523 | GeneralPath b = new GeneralPath(); |
|---|
| 524 | b.moveTo(p1.x, p1.y); b.lineTo(p3.x, p3.y); |
|---|
| 525 | b.lineTo(p4.x, p4.y); b.lineTo(p2.x, p2.y); |
|---|
| 526 | b.lineTo(p1.x, p1.y); |
|---|
| 527 | g2.draw(b); |
|---|
| 528 | g2.setStroke(new BasicStroke(1)); |
|---|
| 529 | } else if (mode == Mode.translate) { |
|---|
| 530 | // Highlight the new and old segments. |
|---|
| 531 | Line2D newline = new Line2D.Double(p3, p4); |
|---|
| 532 | g2.draw(newline); |
|---|
| 533 | g2.setStroke(new BasicStroke(1)); |
|---|
| 534 | Line2D oldline = new Line2D.Double(p1, p2); |
|---|
| 535 | g2.draw(oldline); |
|---|
| 536 | |
|---|
| 537 | if (activeMoveDirection != null) { |
|---|
| 538 | |
|---|
| 539 | double fac = 1.0 / activeMoveDirection.distance(0,0); |
|---|
| 540 | // mult by factor to get unit vector. |
|---|
| 541 | EastNorth normalUnitVector = new EastNorth(activeMoveDirection.getX() * fac, activeMoveDirection.getY() * fac); |
|---|
| 542 | |
|---|
| 543 | // Check to see if our new N1 is in a positive direction with respect to the normalUnitVector. |
|---|
| 544 | // Even if the x component is zero, we should still be able to discern using +0.0 and -0.0 |
|---|
| 545 | if (newN1en != null && (newN1en.getX() > initialN1en.getX() != normalUnitVector.getX() > -0.0)) { |
|---|
| 546 | // If not, use a sign-flipped version of the normalUnitVector. |
|---|
| 547 | normalUnitVector = new EastNorth(-normalUnitVector.getX(), -normalUnitVector.getY()); |
|---|
| 548 | } |
|---|
| 549 | |
|---|
| 550 | //HACK: swap Y, because the target pixels are top down, but EastNorth is bottom-up. |
|---|
| 551 | //This is normally done by MapView.getPoint, but it does not work on vectors. |
|---|
| 552 | normalUnitVector.setLocation(normalUnitVector.getX(), -normalUnitVector.getY()); |
|---|
| 553 | |
|---|
| 554 | // Draw a guideline along the normal. |
|---|
| 555 | Line2D normline; |
|---|
| 556 | Point2D centerpoint = new Point2D.Double((p1.getX()+p2.getX())*0.5, (p1.getY()+p2.getY())*0.5); |
|---|
| 557 | normline = createSemiInfiniteLine(centerpoint, normalUnitVector, g2); |
|---|
| 558 | g2.draw(normline); |
|---|
| 559 | |
|---|
| 560 | // Draw right angle marker on initial position, only when moving at right angle |
|---|
| 561 | if (activeMoveDirection == possibleMoveDirections.get(0)) { |
|---|
| 562 | // EastNorth units per pixel |
|---|
| 563 | double factor = 1.0/g2.getTransform().getScaleX(); |
|---|
| 564 | |
|---|
| 565 | double raoffsetx = 8.0*factor*normalUnitVector.getX(); |
|---|
| 566 | double raoffsety = 8.0*factor*normalUnitVector.getY(); |
|---|
| 567 | Point2D ra1 = new Point2D.Double(centerpoint.getX()+raoffsetx, centerpoint.getY()+raoffsety); |
|---|
| 568 | Point2D ra3 = new Point2D.Double(centerpoint.getX()-raoffsety, centerpoint.getY()+raoffsetx); |
|---|
| 569 | Point2D ra2 = new Point2D.Double(ra1.getX()-raoffsety, ra1.getY()+raoffsetx); |
|---|
| 570 | GeneralPath ra = new GeneralPath(); |
|---|
| 571 | ra.moveTo((float)ra1.getX(), (float)ra1.getY()); |
|---|
| 572 | ra.lineTo((float)ra2.getX(), (float)ra2.getY()); |
|---|
| 573 | ra.lineTo((float)ra3.getX(), (float)ra3.getY()); |
|---|
| 574 | g2.draw(ra); |
|---|
| 575 | } |
|---|
| 576 | } |
|---|
| 577 | } |
|---|
| 578 | } |
|---|
| 579 | } |
|---|
| 580 | } |
|---|
| 581 | |
|---|
| 582 | /** |
|---|
| 583 | * Create a new Line that extends off the edge of the viewport in one direction |
|---|
| 584 | * @param start The start point of the line |
|---|
| 585 | * @param unitvector A unit vector denoting the direction of the line |
|---|
| 586 | * @param g the Graphics2D object it will be used on |
|---|
| 587 | */ |
|---|
| 588 | static private Line2D createSemiInfiniteLine(Point2D start, Point2D unitvector, Graphics2D g) { |
|---|
| 589 | Rectangle bounds = g.getDeviceConfiguration().getBounds(); |
|---|
| 590 | try { |
|---|
| 591 | AffineTransform invtrans = g.getTransform().createInverse(); |
|---|
| 592 | Point2D widthpoint = invtrans.deltaTransform(new Point2D.Double(bounds.width,0), null); |
|---|
| 593 | Point2D heightpoint = invtrans.deltaTransform(new Point2D.Double(0,bounds.height), null); |
|---|
| 594 | |
|---|
| 595 | // Here we should end up with a gross overestimate of the maximum viewport diagonal in what |
|---|
| 596 | // Graphics2D calls 'user space'. Essentially a manhattan distance of manhattan distances. |
|---|
| 597 | // This can be used as a safe length of line to generate which will always go off-viewport. |
|---|
| 598 | double linelength = Math.abs(widthpoint.getX()) + Math.abs(widthpoint.getY()) + Math.abs(heightpoint.getX()) + Math.abs(heightpoint.getY()); |
|---|
| 599 | |
|---|
| 600 | return new Line2D.Double(start, new Point2D.Double(start.getX() + (unitvector.getX() * linelength) , start.getY() + (unitvector.getY() * linelength))); |
|---|
| 601 | } |
|---|
| 602 | catch (NoninvertibleTransformException e) { |
|---|
| 603 | return new Line2D.Double(start, new Point2D.Double(start.getX() + (unitvector.getX() * 10) , start.getY() + (unitvector.getY() * 10))); |
|---|
| 604 | } |
|---|
| 605 | } |
|---|
| 606 | } |
|---|