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