001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.actions.mapmode; 003 004import static org.openstreetmap.josm.gui.help.HelpUtil.ht; 005import static org.openstreetmap.josm.tools.I18n.marktr; 006import static org.openstreetmap.josm.tools.I18n.tr; 007 008import java.awt.AWTEvent; 009import java.awt.BasicStroke; 010import java.awt.Color; 011import java.awt.Cursor; 012import java.awt.Graphics2D; 013import java.awt.Point; 014import java.awt.Rectangle; 015import java.awt.Stroke; 016import java.awt.Toolkit; 017import java.awt.event.AWTEventListener; 018import java.awt.event.ActionEvent; 019import java.awt.event.InputEvent; 020import java.awt.event.KeyEvent; 021import java.awt.event.MouseEvent; 022import java.awt.geom.AffineTransform; 023import java.awt.geom.GeneralPath; 024import java.awt.geom.Line2D; 025import java.awt.geom.NoninvertibleTransformException; 026import java.awt.geom.Point2D; 027import java.util.ArrayList; 028import java.util.Collection; 029import java.util.LinkedList; 030import java.util.List; 031 032import org.openstreetmap.josm.Main; 033import org.openstreetmap.josm.command.AddCommand; 034import org.openstreetmap.josm.command.ChangeCommand; 035import org.openstreetmap.josm.command.Command; 036import org.openstreetmap.josm.command.MoveCommand; 037import org.openstreetmap.josm.command.SequenceCommand; 038import org.openstreetmap.josm.data.Bounds; 039import org.openstreetmap.josm.data.coor.EastNorth; 040import org.openstreetmap.josm.data.osm.Node; 041import org.openstreetmap.josm.data.osm.OsmPrimitive; 042import org.openstreetmap.josm.data.osm.Way; 043import org.openstreetmap.josm.data.osm.WaySegment; 044import org.openstreetmap.josm.data.osm.visitor.paint.PaintColors; 045import org.openstreetmap.josm.gui.MapFrame; 046import org.openstreetmap.josm.gui.MapView; 047import org.openstreetmap.josm.gui.layer.Layer; 048import org.openstreetmap.josm.gui.layer.MapViewPaintable; 049import org.openstreetmap.josm.gui.layer.OsmDataLayer; 050import org.openstreetmap.josm.gui.util.GuiHelper; 051import org.openstreetmap.josm.tools.Geometry; 052import org.openstreetmap.josm.tools.ImageProvider; 053import org.openstreetmap.josm.tools.Shortcut; 054 055/** 056 * Makes a rectangle from a line, or modifies a rectangle. 057 */ 058public class ExtrudeAction extends MapMode implements MapViewPaintable { 059 060 enum Mode { extrude, translate, select, create_new, translate_node } 061 062 private Mode mode = Mode.select; 063 064 /** 065 * If true, when extruding create new node even if segments parallel. 066 */ 067 private boolean alwaysCreateNodes = false; 068 private boolean nodeDragWithoutCtrl; 069 070 private long mouseDownTime = 0; 071 private WaySegment selectedSegment = null; 072 private Node selectedNode = null; 073 private Color mainColor; 074 private Stroke mainStroke; 075 076 /** settings value whether shared nodes should be ignored or not */ 077 private boolean ignoreSharedNodes; 078 079 /** 080 * drawing settings for helper lines 081 */ 082 private Color helperColor; 083 private Stroke helperStrokeDash; 084 private Stroke helperStrokeRA; 085 086 private Stroke oldLineStroke; 087 private double symbolSize; 088 /** 089 * Possible directions to move to. 090 */ 091 private List<ReferenceSegment> possibleMoveDirections; 092 093 094 /** 095 * Collection of nodes that is moved 096 */ 097 private Collection<OsmPrimitive> movingNodeList; 098 099 /** 100 * The direction that is currently active. 101 */ 102 private ReferenceSegment activeMoveDirection; 103 104 /** 105 * The position of the mouse cursor when the drag action was initiated. 106 */ 107 private Point initialMousePos; 108 /** 109 * The time which needs to pass between click and release before something 110 * counts as a move, in milliseconds 111 */ 112 private int initialMoveDelay = 200; 113 /** 114 * The initial EastNorths of node1 and node2 115 */ 116 private EastNorth initialN1en; 117 private EastNorth initialN2en; 118 /** 119 * The new EastNorths of node1 and node2 120 */ 121 private EastNorth newN1en; 122 private EastNorth newN2en; 123 124 /** 125 * the command that performed last move. 126 */ 127 private MoveCommand moveCommand; 128 129 /** The cursor for the 'create_new' mode. */ 130 private final Cursor cursorCreateNew; 131 132 /** The cursor for the 'translate' mode. */ 133 private final Cursor cursorTranslate; 134 135 /** The cursor for the 'alwaysCreateNodes' submode. */ 136 private final Cursor cursorCreateNodes; 137 138 private class ReferenceSegment { 139 public final EastNorth en; 140 public final EastNorth p1; 141 public final EastNorth p2; 142 public final boolean perpendicular; 143 144 public ReferenceSegment(EastNorth en, EastNorth p1, EastNorth p2, boolean perpendicular) { 145 this.en = en; 146 this.p1 = p1; 147 this.p2 = p2; 148 this.perpendicular = perpendicular; 149 } 150 } 151 152 /** 153 * This listener is used to indicate the 'create_new' mode, if the Alt modifier is pressed. 154 */ 155 private final AWTEventListener altKeyListener = new AWTEventListener() { 156 @Override 157 public void eventDispatched(AWTEvent e) { 158 if (!Main.isDisplayingMapView() || !Main.map.mapView.isActiveLayerDrawable()) 159 return; 160 InputEvent ie = (InputEvent) e; 161 boolean alt = (ie.getModifiers() & (ActionEvent.ALT_MASK|InputEvent.ALT_GRAPH_MASK)) != 0; 162 boolean ctrl = (ie.getModifiers() & (ActionEvent.CTRL_MASK)) != 0; 163 boolean shift = (ie.getModifiers() & (ActionEvent.SHIFT_MASK)) != 0; 164 if (mode == Mode.select) { 165 Main.map.mapView.setNewCursor(ctrl ? cursorTranslate : alt ? cursorCreateNew : shift ? cursorCreateNodes : cursor, this); 166 } 167 } 168 }; 169 170 /** 171 * Create a new SelectAction 172 * @param mapFrame The MapFrame this action belongs to. 173 */ 174 public ExtrudeAction(MapFrame mapFrame) { 175 super(tr("Extrude"), "extrude/extrude", tr("Create areas"), 176 Shortcut.registerShortcut("mapmode:extrude", tr("Mode: {0}", tr("Extrude")), KeyEvent.VK_X, Shortcut.DIRECT), 177 mapFrame, 178 ImageProvider.getCursor("normal", "rectangle")); 179 putValue("help", ht("/Action/Extrude")); 180 cursorCreateNew = ImageProvider.getCursor("normal", "rectangle_plus"); 181 cursorTranslate = ImageProvider.getCursor("normal", "rectangle_move"); 182 cursorCreateNodes = ImageProvider.getCursor("normal", "rectangle_plussmall"); 183 } 184 185 @Override public String getModeHelpText() { 186 if (mode == Mode.translate) 187 return tr("Move a segment along its normal, then release the mouse button."); 188 else if (mode == Mode.extrude) 189 return tr("Draw a rectangle of the desired size, then release the mouse button."); 190 else if (mode == Mode.create_new) 191 return tr("Draw a rectangle of the desired size, then release the mouse button."); 192 else 193 return tr("Drag a way segment to make a rectangle. Ctrl-drag to move a segment along its normal, " + 194 "Alt-drag to create a new rectangle, double click to add a new node."); 195 } 196 197 @Override public boolean layerIsSupported(Layer l) { 198 return l instanceof OsmDataLayer; 199 } 200 201 @Override public void enterMode() { 202 super.enterMode(); 203 Main.map.mapView.addMouseListener(this); 204 Main.map.mapView.addMouseMotionListener(this); 205 try { 206 Toolkit.getDefaultToolkit().addAWTEventListener(altKeyListener, AWTEvent.KEY_EVENT_MASK); 207 } catch (SecurityException ex) { 208 Main.warn(ex); 209 } 210 initialMoveDelay = Main.pref.getInteger("edit.initial-move-delay",200); 211 mainColor = Main.pref.getColor(marktr("Extrude: main line"), null); 212 if (mainColor == null) mainColor = PaintColors.SELECTED.get(); 213 helperColor = Main.pref.getColor(marktr("Extrude: helper line"), Color.ORANGE); 214 helperStrokeDash = GuiHelper.getCustomizedStroke(Main.pref.get("extrude.stroke.helper-line", "1 4")); 215 helperStrokeRA = new BasicStroke(1); 216 symbolSize = Main.pref.getDouble("extrude.angle-symbol-radius", 8); 217 nodeDragWithoutCtrl = Main.pref.getBoolean("extrude.drag-nodes-without-ctrl", false); 218 oldLineStroke = GuiHelper.getCustomizedStroke(Main.pref.get("extrude.ctrl.stroke.old-line", "1")); 219 mainStroke = GuiHelper.getCustomizedStroke(Main.pref.get("extrude.stroke.main", "3")); 220 221 ignoreSharedNodes = Main.pref.getBoolean("extrude.ignore-shared-nodes", true); 222 } 223 224 @Override public void exitMode() { 225 Main.map.mapView.removeMouseListener(this); 226 Main.map.mapView.removeMouseMotionListener(this); 227 Main.map.mapView.removeTemporaryLayer(this); 228 try { 229 Toolkit.getDefaultToolkit().removeAWTEventListener(altKeyListener); 230 } catch (SecurityException ex) { 231 Main.warn(ex); 232 } 233 super.exitMode(); 234 } 235 236 /** 237 * If the left mouse button is pressed over a segment, switch 238 * to either extrude, translate or create_new mode depending on whether Ctrl or Alt is held. 239 */ 240 @Override public void mousePressed(MouseEvent e) { 241 if(!Main.map.mapView.isActiveLayerVisible()) 242 return; 243 if (!(Boolean)this.getValue("active")) 244 return; 245 if (e.getButton() != MouseEvent.BUTTON1) 246 return; 247 248 requestFocusInMapView(); 249 updateKeyModifiers(e); 250 251 selectedNode = Main.map.mapView.getNearestNode(e.getPoint(), OsmPrimitive.isSelectablePredicate); 252 selectedSegment = Main.map.mapView.getNearestWaySegment(e.getPoint(), OsmPrimitive.isSelectablePredicate); 253 254 // If nothing gets caught, stay in select mode 255 if (selectedSegment == null && selectedNode == null) return; 256 257 if (selectedNode != null) { 258 if (ctrl || nodeDragWithoutCtrl) { 259 movingNodeList = new ArrayList<OsmPrimitive>(); 260 movingNodeList.add(selectedNode); 261 calculatePossibleDirectionsByNode(); 262 if (possibleMoveDirections.isEmpty()) { 263 // if no directions fould, do not enter dragging mode 264 return; 265 } 266 mode = Mode.translate_node; 267 } 268 } else { 269 // Otherwise switch to another mode 270 if (ctrl) { 271 mode = Mode.translate; 272 movingNodeList = new ArrayList<OsmPrimitive>(); 273 movingNodeList.add(selectedSegment.getFirstNode()); 274 movingNodeList.add(selectedSegment.getSecondNode()); 275 } else if (alt) { 276 mode = Mode.create_new; 277 // create a new segment and then select and extrude the new segment 278 getCurrentDataSet().setSelected(selectedSegment.way); 279 alwaysCreateNodes = true; 280 } else { 281 mode = Mode.extrude; 282 getCurrentDataSet().setSelected(selectedSegment.way); 283 alwaysCreateNodes = shift; 284 } 285 calculatePossibleDirectionsBySegment(); 286 } 287 288 // Signifies that nothing has happened yet 289 newN1en = null; 290 newN2en = null; 291 moveCommand = null; 292 293 Main.map.mapView.addTemporaryLayer(this); 294 295 updateStatusLine(); 296 Main.map.mapView.repaint(); 297 298 // Make note of time pressed 299 mouseDownTime = System.currentTimeMillis(); 300 301 // Make note of mouse position 302 initialMousePos = e.getPoint(); 303 } 304 305 /** 306 * Perform action depending on what mode we're in. 307 */ 308 @Override public void mouseDragged(MouseEvent e) { 309 if(!Main.map.mapView.isActiveLayerVisible()) 310 return; 311 312 // do not count anything as a drag if it lasts less than 100 milliseconds. 313 if (System.currentTimeMillis() - mouseDownTime < initialMoveDelay) 314 return; 315 316 if (mode == Mode.select) { 317 // Just sit tight and wait for mouse to be released. 318 } else { 319 //move, create new and extrude mode - move the selected segment 320 321 EastNorth mouseEn = Main.map.mapView.getEastNorth(e.getPoint().x, e.getPoint().y); 322 EastNorth bestMovement = calculateBestMovement(mouseEn); 323 324 newN1en = new EastNorth(initialN1en.getX() + bestMovement.getX(), initialN1en.getY() + bestMovement.getY()); 325 newN2en = new EastNorth(initialN2en.getX() + bestMovement.getX(), initialN2en.getY() + bestMovement.getY()); 326 327 // find out the movement distance, in metres 328 double distance = Main.getProjection().eastNorth2latlon(initialN1en).greatCircleDistance(Main.getProjection().eastNorth2latlon(newN1en)); 329 Main.map.statusLine.setDist(distance); 330 updateStatusLine(); 331 332 Main.map.mapView.setNewCursor(Cursor.MOVE_CURSOR, this); 333 334 if (mode == Mode.extrude || mode == Mode.create_new) { 335 //nothing here 336 } else if (mode == Mode.translate_node || mode == Mode.translate) { 337 //move nodes to new position 338 if (moveCommand == null) { 339 //make a new move command 340 moveCommand = new MoveCommand(movingNodeList, bestMovement.getX(), bestMovement.getY()); 341 Main.main.undoRedo.add(moveCommand); 342 } else { 343 //reuse existing move command 344 moveCommand.moveAgainTo(bestMovement.getX(), bestMovement.getY()); 345 } 346 } 347 348 Main.map.mapView.repaint(); 349 } 350 } 351 352 /** 353 * Do anything that needs to be done, then switch back to select mode 354 */ 355 @Override public void mouseReleased(MouseEvent e) { 356 357 if(!Main.map.mapView.isActiveLayerVisible()) 358 return; 359 360 if (mode == Mode.select) { 361 // Nothing to be done 362 } else { 363 if (mode == Mode.create_new) { 364 if (e.getPoint().distance(initialMousePos) > 10 && newN1en != null) { 365 createNewRectangle(); 366 } 367 } else if (mode == Mode.extrude) { 368 if( e.getClickCount() == 2 && e.getPoint().equals(initialMousePos) ) { 369 // double click adds a new node 370 addNewNode(e); 371 } 372 else if (e.getPoint().distance(initialMousePos) > 10 && newN1en != null && selectedSegment != null) { 373 // main extrusion commands 374 performExtrusion(); 375 } 376 } else if (mode == Mode.translate || mode == Mode.translate_node) { 377 //Commit translate 378 //the move command is already committed in mouseDragged 379 } 380 381 boolean alt = (e.getModifiers() & (ActionEvent.ALT_MASK|InputEvent.ALT_GRAPH_MASK)) != 0; 382 boolean ctrl = (e.getModifiers() & (ActionEvent.CTRL_MASK)) != 0; 383 boolean shift = (e.getModifiers() & (ActionEvent.SHIFT_MASK)) != 0; 384 // Switch back into select mode 385 Main.map.mapView.setNewCursor(ctrl ? cursorTranslate : alt ? cursorCreateNew : shift ? cursorCreateNodes : cursor, this); 386 Main.map.mapView.removeTemporaryLayer(this); 387 selectedSegment = null; 388 moveCommand = null; 389 mode = Mode.select; 390 391 updateStatusLine(); 392 Main.map.mapView.repaint(); 393 } 394 } 395 396 /** 397 * Insert node into nearby segment 398 * @param e - current mouse point 399 */ 400 private void addNewNode(MouseEvent e) { 401 // Should maybe do the same as in DrawAction and fetch all nearby segments? 402 WaySegment ws = Main.map.mapView.getNearestWaySegment(e.getPoint(), OsmPrimitive.isSelectablePredicate); 403 if (ws != null) { 404 Node n = new Node(Main.map.mapView.getLatLon(e.getX(), e.getY())); 405 EastNorth A = ws.getFirstNode().getEastNorth(); 406 EastNorth B = ws.getSecondNode().getEastNorth(); 407 n.setEastNorth(Geometry.closestPointToSegment(A, B, n.getEastNorth())); 408 Way wnew = new Way(ws.way); 409 wnew.addNode(ws.lowerIndex+1, n); 410 SequenceCommand cmds = new SequenceCommand(tr("Add a new node to an existing way"), 411 new AddCommand(n), new ChangeCommand(ws.way, wnew)); 412 Main.main.undoRedo.add(cmds); 413 } 414 } 415 416 private void createNewRectangle() { 417 if (selectedSegment == null) return; 418 // crete a new rectangle 419 Collection<Command> cmds = new LinkedList<Command>(); 420 Node third = new Node(newN2en); 421 Node fourth = new Node(newN1en); 422 Way wnew = new Way(); 423 wnew.addNode(selectedSegment.getFirstNode()); 424 wnew.addNode(selectedSegment.getSecondNode()); 425 wnew.addNode(third); 426 wnew.addNode(fourth); 427 // ... and close the way 428 wnew.addNode(selectedSegment.getFirstNode()); 429 // undo support 430 cmds.add(new AddCommand(third)); 431 cmds.add(new AddCommand(fourth)); 432 cmds.add(new AddCommand(wnew)); 433 Command c = new SequenceCommand(tr("Extrude Way"), cmds); 434 Main.main.undoRedo.add(c); 435 getCurrentDataSet().setSelected(wnew); 436 } 437 438 /** 439 * Do actual extrusion of @field selectedSegment 440 */ 441 private void performExtrusion() { 442 // create extrusion 443 Collection<Command> cmds = new LinkedList<Command>(); 444 Way wnew = new Way(selectedSegment.way); 445 boolean wayWasModified = false; 446 boolean wayWasSingleSegment = wnew.getNodesCount() == 2; 447 int insertionPoint = selectedSegment.lowerIndex + 1; 448 449 //find if the new points overlap existing segments (in case of 90 degree angles) 450 Node prevNode = getPreviousNode(selectedSegment.lowerIndex); 451 boolean nodeOverlapsSegment = prevNode != null && Geometry.segmentsParallel(initialN1en, prevNode.getEastNorth(), initialN1en, newN1en); 452 // segmentAngleZero marks subset of nodeOverlapsSegment. nodeOverlapsSegment is true if angle between segments is 0 or PI, segmentAngleZero only if angle is 0 453 boolean segmentAngleZero = prevNode != null && Math.abs(Geometry.getCornerAngle(prevNode.getEastNorth(), initialN1en, newN1en)) < 1e-5; 454 boolean hasOtherWays = this.hasNodeOtherWays(selectedSegment.getFirstNode(), selectedSegment.way); 455 456 if (nodeOverlapsSegment && !alwaysCreateNodes && !hasOtherWays) { 457 //move existing node 458 Node n1Old = selectedSegment.getFirstNode(); 459 cmds.add(new MoveCommand(n1Old, Main.getProjection().eastNorth2latlon(newN1en))); 460 } else if (ignoreSharedNodes && segmentAngleZero && !alwaysCreateNodes && hasOtherWays) { 461 // replace shared node with new one 462 Node n1Old = selectedSegment.getFirstNode(); 463 Node n1New = new Node(Main.getProjection().eastNorth2latlon(newN1en)); 464 wnew.addNode(insertionPoint, n1New); 465 wnew.removeNode(n1Old); 466 wayWasModified = true; 467 cmds.add(new AddCommand(n1New)); 468 } else { 469 //introduce new node 470 Node n1New = new Node(Main.getProjection().eastNorth2latlon(newN1en)); 471 wnew.addNode(insertionPoint, n1New); 472 wayWasModified = true; 473 insertionPoint ++; 474 cmds.add(new AddCommand(n1New)); 475 } 476 477 //find if the new points overlap existing segments (in case of 90 degree angles) 478 Node nextNode = getNextNode(selectedSegment.lowerIndex + 1); 479 nodeOverlapsSegment = nextNode != null && Geometry.segmentsParallel(initialN2en, nextNode.getEastNorth(), initialN2en, newN2en); 480 segmentAngleZero = nextNode != null && Math.abs(Geometry.getCornerAngle(nextNode.getEastNorth(), initialN2en, newN2en)) < 1e-5; 481 hasOtherWays = hasNodeOtherWays(selectedSegment.getSecondNode(), selectedSegment.way); 482 483 if (nodeOverlapsSegment && !alwaysCreateNodes && !hasOtherWays) { 484 //move existing node 485 Node n2Old = selectedSegment.getSecondNode(); 486 cmds.add(new MoveCommand(n2Old, Main.getProjection().eastNorth2latlon(newN2en))); 487 } else if (ignoreSharedNodes && segmentAngleZero && !alwaysCreateNodes && hasOtherWays) { 488 // replace shared node with new one 489 Node n2Old = selectedSegment.getSecondNode(); 490 Node n2New = new Node(Main.getProjection().eastNorth2latlon(newN2en)); 491 wnew.addNode(insertionPoint, n2New); 492 wnew.removeNode(n2Old); 493 wayWasModified = true; 494 cmds.add(new AddCommand(n2New)); 495 } else { 496 //introduce new node 497 Node n2New = new Node(Main.getProjection().eastNorth2latlon(newN2en)); 498 wnew.addNode(insertionPoint, n2New); 499 wayWasModified = true; 500 insertionPoint ++; 501 cmds.add(new AddCommand(n2New)); 502 } 503 504 //the way was a single segment, close the way 505 if (wayWasSingleSegment) { 506 wnew.addNode(selectedSegment.getFirstNode()); 507 wayWasModified = true; 508 } 509 if (wayWasModified) { 510 // we only need to change the way if its node list was really modified 511 cmds.add(new ChangeCommand(selectedSegment.way, wnew)); 512 } 513 Command c = new SequenceCommand(tr("Extrude Way"), cmds); 514 Main.main.undoRedo.add(c); 515 } 516 517 /** 518 * This method tests if a node has other ways apart from the given one. 519 * @param node 520 * @param myWay 521 * @return true of node belongs only to myWay, false if there are more ways. 522 */ 523 private boolean hasNodeOtherWays(Node node, Way myWay) { 524 for (OsmPrimitive p : node.getReferrers()) { 525 if (p instanceof Way && p.isUsable() && p != myWay) 526 return true; 527 } 528 return false; 529 } 530 531 /** 532 * Determine best movenemnt from initialMousePos to current position @param mouseEn, 533 * choosing one of the directions @field possibleMoveDirections 534 * @return movement vector 535 */ 536 private EastNorth calculateBestMovement(EastNorth mouseEn) { 537 538 EastNorth initialMouseEn = Main.map.mapView.getEastNorth(initialMousePos.x, initialMousePos.y); 539 EastNorth mouseMovement = initialMouseEn.sub(mouseEn); 540 541 double bestDistance = Double.POSITIVE_INFINITY; 542 EastNorth bestMovement = null; 543 activeMoveDirection = null; 544 545 //find the best movement direction and vector 546 for (ReferenceSegment direction : possibleMoveDirections) { 547 EastNorth movement = calculateSegmentOffset(initialN1en, initialN2en, direction.en, mouseEn); 548 if (movement == null) { 549 //if direction parallel to segment. 550 continue; 551 } 552 553 double distanceFromMouseMovement = movement.distance(mouseMovement); 554 if (bestDistance > distanceFromMouseMovement) { 555 bestDistance = distanceFromMouseMovement; 556 activeMoveDirection = direction; 557 bestMovement = movement; 558 } 559 } 560 return bestMovement; 561 } 562 563 /*** 564 * This method calculates offset amount by witch to move the given segment perpendicularly for it to be in line with mouse position. 565 * @param segmentP1 566 * @param segmentP2 567 * @param targetPos 568 * @return offset amount of P1 and P2. 569 */ 570 private static EastNorth calculateSegmentOffset(EastNorth segmentP1, EastNorth segmentP2, EastNorth moveDirection, 571 EastNorth targetPos) { 572 EastNorth intersectionPoint; 573 if (segmentP1.distanceSq(segmentP2)>1e-7) { 574 intersectionPoint = Geometry.getLineLineIntersection(segmentP1, segmentP2, targetPos, targetPos.add(moveDirection)); 575 } else { 576 intersectionPoint = Geometry.closestPointToLine(targetPos, targetPos.add(moveDirection), segmentP1); 577 } 578 579 if (intersectionPoint == null) 580 return null; 581 else 582 //return distance form base to target position 583 return intersectionPoint.sub(targetPos); 584 } 585 586 /** 587 * Gather possible move directions - perpendicular to the selected segment and parallel to neighbor segments 588 */ 589 private void calculatePossibleDirectionsBySegment() { 590 // remember initial positions for segment nodes. 591 initialN1en = selectedSegment.getFirstNode().getEastNorth(); 592 initialN2en = selectedSegment.getSecondNode().getEastNorth(); 593 594 //add direction perpendicular to the selected segment 595 possibleMoveDirections = new ArrayList<ReferenceSegment>(); 596 possibleMoveDirections.add(new ReferenceSegment(new EastNorth( 597 initialN1en.getY() - initialN2en.getY(), 598 initialN2en.getX() - initialN1en.getX() 599 ), initialN1en, initialN2en, true)); 600 601 602 //add directions parallel to neighbor segments 603 Node prevNode = getPreviousNode(selectedSegment.lowerIndex); 604 if (prevNode != null) { 605 EastNorth en = prevNode.getEastNorth(); 606 possibleMoveDirections.add(new ReferenceSegment(new EastNorth( 607 initialN1en.getX() - en.getX(), 608 initialN1en.getY() - en.getY() 609 ), initialN1en, en, false)); 610 } 611 612 Node nextNode = getNextNode(selectedSegment.lowerIndex + 1); 613 if (nextNode != null) { 614 EastNorth en = nextNode.getEastNorth(); 615 possibleMoveDirections.add(new ReferenceSegment(new EastNorth( 616 initialN2en.getX() - en.getX(), 617 initialN2en.getY() - en.getY() 618 ), initialN2en, en, false)); 619 } 620 } 621 622 /** 623 * Gather possible move directions - along all adjacent segments 624 */ 625 private void calculatePossibleDirectionsByNode() { 626 // remember initial positions for segment nodes. 627 initialN1en = selectedNode.getEastNorth(); 628 initialN2en = initialN1en; 629 possibleMoveDirections = new ArrayList<ReferenceSegment>(); 630 for (OsmPrimitive p: selectedNode.getReferrers()) { 631 if (p instanceof Way && p.isUsable()) { 632 for (Node neighbor: ((Way) p).getNeighbours(selectedNode)) { 633 EastNorth en = neighbor.getEastNorth(); 634 possibleMoveDirections.add(new ReferenceSegment(new EastNorth( 635 initialN1en.getX() - en.getX(), 636 initialN1en.getY() - en.getY() 637 ), initialN1en, en, false)); 638 } 639 } 640 } 641 } 642 643 /** 644 * Gets a node from selected way before given index. 645 * @param index index of current node 646 * @return index of previous node or -1 if there are no nodes there. 647 */ 648 private int getPreviousNodeIndex(int index) { 649 if (index > 0) 650 return index - 1; 651 else if (selectedSegment.way.isClosed()) 652 return selectedSegment.way.getNodesCount() - 2; 653 else 654 return -1; 655 } 656 657 /** 658 * Gets a node from selected way before given index. 659 * @param index index of current node 660 * @return previous node or null if there are no nodes there. 661 */ 662 private Node getPreviousNode(int index) { 663 int indexPrev = getPreviousNodeIndex(index); 664 if (indexPrev >= 0) 665 return selectedSegment.way.getNode(indexPrev); 666 else 667 return null; 668 } 669 670 671 /** 672 * Gets a node from selected way after given index. 673 * @param index index of current node 674 * @return index of next node or -1 if there are no nodes there. 675 */ 676 private int getNextNodeIndex(int index) { 677 int count = selectedSegment.way.getNodesCount(); 678 if (index < count - 1) 679 return index + 1; 680 else if (selectedSegment.way.isClosed()) 681 return 1; 682 else 683 return -1; 684 } 685 686 /** 687 * Gets a node from selected way after given index. 688 * @param index index of current node 689 * @return next node or null if there are no nodes there. 690 */ 691 private Node getNextNode(int index) { 692 int indexNext = getNextNodeIndex(index); 693 if (indexNext >= 0) 694 return selectedSegment.way.getNode(indexNext); 695 else 696 return null; 697 } 698 699 @Override 700 public void paint(Graphics2D g, MapView mv, Bounds box) { 701 Graphics2D g2 = g; 702 if (mode == Mode.select) { 703 // Nothing to do 704 } else { 705 if (newN1en != null) { 706 707 Point p1 = mv.getPoint(initialN1en); 708 Point p2 = mv.getPoint(initialN2en); 709 Point p3 = mv.getPoint(newN1en); 710 Point p4 = mv.getPoint(newN2en); 711 712 Point2D normalUnitVector = getNormalUniVector(); 713 714 if (mode == Mode.extrude || mode == Mode.create_new) { 715 g2.setColor(mainColor); 716 g2.setStroke(mainStroke); 717 // Draw rectangle around new area. 718 GeneralPath b = new GeneralPath(); 719 b.moveTo(p1.x, p1.y); b.lineTo(p3.x, p3.y); 720 b.lineTo(p4.x, p4.y); b.lineTo(p2.x, p2.y); 721 b.lineTo(p1.x, p1.y); 722 g2.draw(b); 723 724 if (activeMoveDirection != null) { 725 // Draw reference way 726 Point pr1 = mv.getPoint(activeMoveDirection.p1); 727 Point pr2 = mv.getPoint(activeMoveDirection.p2); 728 b = new GeneralPath(); 729 b.moveTo(pr1.x, pr1.y); 730 b.lineTo(pr2.x, pr2.y); 731 g2.setColor(helperColor); 732 g2.setStroke(helperStrokeDash); 733 g2.draw(b); 734 735 // Draw right angle marker on first node position, only when moving at right angle 736 if (activeMoveDirection.perpendicular) { 737 // mirror RightAngle marker, so it is inside the extrude 738 double headingRefWS = activeMoveDirection.p1.heading(activeMoveDirection.p2); 739 double headingMoveDir = Math.atan2(normalUnitVector.getY(), normalUnitVector.getX()); 740 double headingDiff = headingRefWS - headingMoveDir; 741 if (headingDiff < 0) headingDiff += 2 * Math.PI; 742 boolean mirrorRA = Math.abs(headingDiff - Math.PI) > 1e-5; 743 drawAngleSymbol(g2, pr1, normalUnitVector, mirrorRA); 744 } 745 } 746 } else if (mode == Mode.translate || mode == Mode.translate_node) { 747 g2.setColor(mainColor); 748 if (p1.distance(p2) < 3) { 749 g2.setStroke(mainStroke); 750 g2.drawOval((int)(p1.x-symbolSize/2), (int)(p1.y-symbolSize/2), 751 (int)(symbolSize), (int)(symbolSize)); 752 } else { 753 Line2D oldline = new Line2D.Double(p1, p2); 754 g2.setStroke(oldLineStroke); 755 g2.draw(oldline); 756 } 757 758 if (activeMoveDirection != null) { 759 760 g2.setColor(helperColor); 761 g2.setStroke(helperStrokeDash); 762 // Draw a guideline along the normal. 763 Line2D normline; 764 Point2D centerpoint = new Point2D.Double((p1.getX()+p2.getX())*0.5, (p1.getY()+p2.getY())*0.5); 765 normline = createSemiInfiniteLine(centerpoint, normalUnitVector, g2); 766 g2.draw(normline); 767 // Draw right angle marker on initial position, only when moving at right angle 768 if (activeMoveDirection.perpendicular) { 769 // EastNorth units per pixel 770 g2.setStroke(helperStrokeRA); 771 g2.setColor(mainColor); 772 drawAngleSymbol(g2, centerpoint, normalUnitVector, false); 773 } 774 } 775 } 776 } 777 g2.setStroke(helperStrokeRA); // restore default stroke to prevent starnge occasional drawings 778 } 779 } 780 781 private Point2D getNormalUniVector() { 782 double fac = 1.0 / activeMoveDirection.en.length(); 783 // mult by factor to get unit vector. 784 Point2D normalUnitVector = new Point2D.Double(activeMoveDirection.en.getX() * fac, activeMoveDirection.en.getY() * fac); 785 786 // Check to see if our new N1 is in a positive direction with respect to the normalUnitVector. 787 // Even if the x component is zero, we should still be able to discern using +0.0 and -0.0 788 if (newN1en != null && ((newN1en.getX() > initialN1en.getX()) != (normalUnitVector.getX() > -0.0))) { 789 // If not, use a sign-flipped version of the normalUnitVector. 790 normalUnitVector = new Point2D.Double(-normalUnitVector.getX(), -normalUnitVector.getY()); 791 } 792 793 //HACK: swap Y, because the target pixels are top down, but EastNorth is bottom-up. 794 //This is normally done by MapView.getPoint, but it does not work on vectors. 795 normalUnitVector.setLocation(normalUnitVector.getX(), -normalUnitVector.getY()); 796 return normalUnitVector; 797 } 798 799 private void drawAngleSymbol(Graphics2D g2, Point2D center, Point2D normal, boolean mirror) { 800 // EastNorth units per pixel 801 double factor = 1.0/g2.getTransform().getScaleX(); 802 double raoffsetx = symbolSize*factor*normal.getX(); 803 double raoffsety = symbolSize*factor*normal.getY(); 804 805 double cx = center.getX(), cy = center.getY(); 806 double k = (mirror ? -1 : 1); 807 Point2D ra1 = new Point2D.Double(cx + raoffsetx, cy + raoffsety); 808 Point2D ra3 = new Point2D.Double(cx - raoffsety*k, cy + raoffsetx*k); 809 Point2D ra2 = new Point2D.Double(ra1.getX() - raoffsety*k, ra1.getY() + raoffsetx*k); 810 811 GeneralPath ra = new GeneralPath(); 812 ra.moveTo((float)ra1.getX(), (float)ra1.getY()); 813 ra.lineTo((float)ra2.getX(), (float)ra2.getY()); 814 ra.lineTo((float)ra3.getX(), (float)ra3.getY()); 815 g2.setStroke(helperStrokeRA); 816 g2.draw(ra); 817 } 818 819 /** 820 * Create a new Line that extends off the edge of the viewport in one direction 821 * @param start The start point of the line 822 * @param unitvector A unit vector denoting the direction of the line 823 * @param g the Graphics2D object it will be used on 824 */ 825 static private Line2D createSemiInfiniteLine(Point2D start, Point2D unitvector, Graphics2D g) { 826 Rectangle bounds = g.getDeviceConfiguration().getBounds(); 827 try { 828 AffineTransform invtrans = g.getTransform().createInverse(); 829 Point2D widthpoint = invtrans.deltaTransform(new Point2D.Double(bounds.width,0), null); 830 Point2D heightpoint = invtrans.deltaTransform(new Point2D.Double(0,bounds.height), null); 831 832 // Here we should end up with a gross overestimate of the maximum viewport diagonal in what 833 // Graphics2D calls 'user space'. Essentially a manhattan distance of manhattan distances. 834 // This can be used as a safe length of line to generate which will always go off-viewport. 835 double linelength = Math.abs(widthpoint.getX()) + Math.abs(widthpoint.getY()) + Math.abs(heightpoint.getX()) + Math.abs(heightpoint.getY()); 836 837 return new Line2D.Double(start, new Point2D.Double(start.getX() + (unitvector.getX() * linelength) , start.getY() + (unitvector.getY() * linelength))); 838 } 839 catch (NoninvertibleTransformException e) { 840 return new Line2D.Double(start, new Point2D.Double(start.getX() + (unitvector.getX() * 10) , start.getY() + (unitvector.getY() * 10))); 841 } 842 } 843}