001 // License: GPL. See LICENSE file for details. 002 package org.openstreetmap.josm.actions.mapmode; 003 004 import static org.openstreetmap.josm.tools.I18n.tr; 005 import static org.openstreetmap.josm.tools.I18n.trn; 006 007 import java.awt.AWTEvent; 008 import java.awt.BasicStroke; 009 import java.awt.Color; 010 import java.awt.Cursor; 011 import java.awt.Graphics2D; 012 import java.awt.Point; 013 import java.awt.Toolkit; 014 import java.awt.event.AWTEventListener; 015 import java.awt.event.InputEvent; 016 import java.awt.event.KeyEvent; 017 import java.awt.event.MouseEvent; 018 import java.awt.geom.GeneralPath; 019 import java.util.ArrayList; 020 import java.util.Collection; 021 import java.util.Iterator; 022 import java.util.LinkedList; 023 import java.util.List; 024 025 import javax.swing.JOptionPane; 026 027 import org.openstreetmap.josm.Main; 028 import org.openstreetmap.josm.command.AddCommand; 029 import org.openstreetmap.josm.command.ChangeCommand; 030 import org.openstreetmap.josm.command.Command; 031 import org.openstreetmap.josm.command.DeleteCommand; 032 import org.openstreetmap.josm.command.MoveCommand; 033 import org.openstreetmap.josm.command.SequenceCommand; 034 import org.openstreetmap.josm.data.Bounds; 035 import org.openstreetmap.josm.data.SelectionChangedListener; 036 import org.openstreetmap.josm.data.coor.EastNorth; 037 import org.openstreetmap.josm.data.osm.DataSet; 038 import org.openstreetmap.josm.data.osm.Node; 039 import org.openstreetmap.josm.data.osm.OsmPrimitive; 040 import org.openstreetmap.josm.data.osm.Way; 041 import org.openstreetmap.josm.data.osm.WaySegment; 042 import org.openstreetmap.josm.data.osm.visitor.paint.PaintColors; 043 import org.openstreetmap.josm.gui.MapFrame; 044 import org.openstreetmap.josm.gui.MapView; 045 import org.openstreetmap.josm.gui.layer.Layer; 046 import org.openstreetmap.josm.gui.layer.MapViewPaintable; 047 import org.openstreetmap.josm.gui.layer.OsmDataLayer; 048 import org.openstreetmap.josm.tools.ImageProvider; 049 import org.openstreetmap.josm.tools.Pair; 050 import org.openstreetmap.josm.tools.Shortcut; 051 052 /** 053 * @author Alexander Kachkaev <alexander@kachkaev.ru>, 2011 054 */ 055 public class ImproveWayAccuracyAction extends MapMode implements MapViewPaintable, 056 SelectionChangedListener, AWTEventListener { 057 058 enum State { 059 selecting, improving 060 } 061 062 private State state; 063 064 private MapView mv; 065 066 private static final long serialVersionUID = 42L; 067 068 private Way targetWay; 069 private Node candidateNode = null; 070 private WaySegment candidateSegment = null; 071 072 private Point mousePos = null; 073 private boolean dragging = false; 074 075 final private Cursor cursorSelect; 076 final private Cursor cursorSelectHover; 077 final private Cursor cursorImprove; 078 final private Cursor cursorImproveAdd; 079 final private Cursor cursorImproveDelete; 080 final private Cursor cursorImproveAddLock; 081 final private Cursor cursorImproveLock; 082 083 private final Color guideColor; 084 private final BasicStroke selectTargetWayStroke; 085 private final BasicStroke moveNodeStroke; 086 private final BasicStroke addNodeStroke; 087 private final BasicStroke deleteNodeStroke; 088 089 private boolean selectionChangedBlocked = false; 090 091 protected String oldModeHelpText; 092 093 public ImproveWayAccuracyAction(MapFrame mapFrame) { 094 super(tr("Improve Way Accuracy"), "improvewayaccuracy.png", 095 tr("Improve Way Accuracy mode"), 096 Shortcut.registerShortcut("mapmode:ImproveWayAccuracy", 097 tr("Mode: {0}", tr("Improve Way Accuracy")), 098 KeyEvent.VK_W, Shortcut.DIRECT), mapFrame, Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR)); 099 100 cursorSelect = ImageProvider.getCursor("normal", "mode"); 101 cursorSelectHover = ImageProvider.getCursor("hand", "mode"); 102 cursorImprove = ImageProvider.getCursor("crosshair", null); 103 cursorImproveAdd = ImageProvider.getCursor("crosshair", "addnode"); 104 cursorImproveDelete = ImageProvider.getCursor("crosshair", "delete_node"); 105 cursorImproveAddLock = ImageProvider.getCursor("crosshair", 106 "add_node_lock"); 107 cursorImproveLock = ImageProvider.getCursor("crosshair", "lock"); 108 109 guideColor = PaintColors.HIGHLIGHT.get(); 110 selectTargetWayStroke = new BasicStroke(2, BasicStroke.CAP_ROUND, 111 BasicStroke.JOIN_ROUND); 112 float dash1[] = {4.0f}; 113 moveNodeStroke = new BasicStroke(1.0f, BasicStroke.CAP_BUTT, 114 BasicStroke.JOIN_MITER, 10.0f, dash1, 0.0f); 115 addNodeStroke = new BasicStroke(1, BasicStroke.CAP_BUTT, 116 BasicStroke.JOIN_MITER); 117 deleteNodeStroke = new BasicStroke(1, BasicStroke.CAP_BUTT, 118 BasicStroke.JOIN_MITER); 119 } 120 121 // ------------------------------------------------------------------------- 122 // Mode methods 123 // ------------------------------------------------------------------------- 124 @Override 125 public void enterMode() { 126 if (!isEnabled()) { 127 return; 128 } 129 super.enterMode(); 130 131 mv = Main.map.mapView; 132 mousePos = null; 133 oldModeHelpText = ""; 134 135 if (getCurrentDataSet() == null) { 136 return; 137 } 138 139 updateStateByCurrentSelection(); 140 141 Main.map.mapView.addMouseListener(this); 142 Main.map.mapView.addMouseMotionListener(this); 143 Main.map.mapView.addTemporaryLayer(this); 144 DataSet.addSelectionListener(this); 145 146 try { 147 Toolkit.getDefaultToolkit().addAWTEventListener(this, 148 AWTEvent.KEY_EVENT_MASK); 149 } catch (SecurityException ex) { 150 } 151 } 152 153 @Override 154 public void exitMode() { 155 super.exitMode(); 156 157 Main.map.mapView.removeMouseListener(this); 158 Main.map.mapView.removeMouseMotionListener(this); 159 Main.map.mapView.removeTemporaryLayer(this); 160 DataSet.removeSelectionListener(this); 161 162 try { 163 Toolkit.getDefaultToolkit().removeAWTEventListener(this); 164 } catch (SecurityException ex) { 165 } 166 167 Main.map.mapView.repaint(); 168 } 169 170 @Override 171 protected void updateStatusLine() { 172 String newModeHelpText = getModeHelpText(); 173 if (!newModeHelpText.equals(oldModeHelpText)) { 174 oldModeHelpText = newModeHelpText; 175 Main.map.statusLine.setHelpText(newModeHelpText); 176 Main.map.statusLine.repaint(); 177 } 178 } 179 180 @Override 181 public String getModeHelpText() { 182 if (state == State.selecting) { 183 if (targetWay != null) { 184 return tr("Click on the way to start improving its shape."); 185 } else { 186 return tr("Select a way that you want to make more accurate."); 187 } 188 } else { 189 if (ctrl) { 190 return tr("Click to add a new node. Release Ctrl to move existing nodes or hold Alt to delete."); 191 } else if (alt) { 192 return tr("Click to delete the highlighted node. Release Alt to move existing nodes or hold Ctrl to add new nodes."); 193 } else { 194 return tr("Click to move the highlighted node. Hold Ctrl to add new nodes, or Alt to delete."); 195 } 196 } 197 } 198 199 @Override 200 public boolean layerIsSupported(Layer l) { 201 return l instanceof OsmDataLayer; 202 } 203 204 @Override 205 protected void updateEnabledState() { 206 setEnabled(getEditLayer() != null); 207 // setEnabled(Main.main.getActiveLayer() instanceof OsmDataLayer); 208 } 209 210 // ------------------------------------------------------------------------- 211 // MapViewPaintable methods 212 // ------------------------------------------------------------------------- 213 /** 214 * Redraws temporary layer. Highlights targetWay in select mode. Draws 215 * preview lines in improve mode and highlights the candidateNode 216 */ 217 @Override 218 public void paint(Graphics2D g, MapView mv, Bounds bbox) { 219 if (mousePos == null) { 220 return; 221 } 222 223 g.setColor(guideColor); 224 225 if (state == State.selecting && targetWay != null) { 226 // Highlighting the targetWay in Selecting state 227 // Non-native highlighting is used, because sometimes highlighted 228 // segments are covered with others, which is bad. 229 g.setStroke(selectTargetWayStroke); 230 231 List<Node> nodes = targetWay.getNodes(); 232 233 GeneralPath b = new GeneralPath(); 234 Point p0 = mv.getPoint(nodes.get(0)); 235 Point pn; 236 b.moveTo(p0.x, p0.y); 237 238 for (Node n : nodes) { 239 pn = mv.getPoint(n); 240 b.lineTo(pn.x, pn.y); 241 } 242 if (targetWay.isClosed()) { 243 b.lineTo(p0.x, p0.y); 244 } 245 246 g.draw(b); 247 248 } else if (state == State.improving) { 249 // Drawing preview lines and highlighting the node 250 // that is going to be moved. 251 // Non-native highlighting is used here as well. 252 253 // Finding endpoints 254 Point p1 = null, p2 = null; 255 if (ctrl && candidateSegment != null) { 256 g.setStroke(addNodeStroke); 257 p1 = mv.getPoint(candidateSegment.getFirstNode()); 258 p2 = mv.getPoint(candidateSegment.getSecondNode()); 259 } else if (!alt && !ctrl && candidateNode != null) { 260 g.setStroke(moveNodeStroke); 261 List<Pair<Node, Node>> wpps = targetWay.getNodePairs(false); 262 for (Pair<Node, Node> wpp : wpps) { 263 if (wpp.a == candidateNode) { 264 p1 = mv.getPoint(wpp.b); 265 } 266 if (wpp.b == candidateNode) { 267 p2 = mv.getPoint(wpp.a); 268 } 269 if (p1 != null && p2 != null) { 270 break; 271 } 272 } 273 } else if (alt && !ctrl && candidateNode != null) { 274 g.setStroke(deleteNodeStroke); 275 List<Node> nodes = targetWay.getNodes(); 276 int index = nodes.indexOf(candidateNode); 277 278 // Only draw line if node is not first and/or last 279 if (index != 0 && index != (nodes.size() - 1)) { 280 p1 = mv.getPoint(nodes.get(index - 1)); 281 p2 = mv.getPoint(nodes.get(index + 1)); 282 } 283 // TODO: indicate what part that will be deleted? (for end nodes) 284 } 285 286 287 // Drawing preview lines 288 GeneralPath b = new GeneralPath(); 289 if (alt && !ctrl) { 290 // In delete mode 291 if (p1 != null && p2 != null) { 292 b.moveTo(p1.x, p1.y); 293 b.lineTo(p2.x, p2.y); 294 } 295 } else { 296 // In add or move mode 297 if (p1 != null) { 298 b.moveTo(mousePos.x, mousePos.y); 299 b.lineTo(p1.x, p1.y); 300 } 301 if (p2 != null) { 302 b.moveTo(mousePos.x, mousePos.y); 303 b.lineTo(p2.x, p2.y); 304 } 305 } 306 g.draw(b); 307 308 // Highlighting candidateNode 309 if (candidateNode != null) { 310 p1 = mv.getPoint(candidateNode); 311 g.fillRect(p1.x - 2, p1.y - 2, 6, 6); 312 } 313 314 } 315 } 316 317 // ------------------------------------------------------------------------- 318 // Event handlers 319 // ------------------------------------------------------------------------- 320 @Override 321 public void eventDispatched(AWTEvent event) { 322 if (Main.map == null || Main.map.mapView == null 323 || !Main.map.mapView.isActiveLayerDrawable()) { 324 return; 325 } 326 updateKeyModifiers((InputEvent) event); 327 updateCursorDependentObjectsIfNeeded(); 328 updateCursor(); 329 updateStatusLine(); 330 Main.map.mapView.repaint(); 331 } 332 333 @Override 334 public void selectionChanged(Collection<? extends OsmPrimitive> newSelection) { 335 if (selectionChangedBlocked) { 336 return; 337 } 338 updateStateByCurrentSelection(); 339 } 340 341 @Override 342 public void mouseDragged(MouseEvent e) { 343 dragging = true; 344 mouseMoved(e); 345 } 346 347 @Override 348 public void mouseMoved(MouseEvent e) { 349 if (!isEnabled()) { 350 return; 351 } 352 353 mousePos = e.getPoint(); 354 355 updateKeyModifiers(e); 356 updateCursorDependentObjectsIfNeeded(); 357 updateCursor(); 358 updateStatusLine(); 359 Main.map.mapView.repaint(); 360 } 361 362 @Override 363 public void mouseReleased(MouseEvent e) { 364 dragging = false; 365 if (!isEnabled() || e.getButton() != MouseEvent.BUTTON1) { 366 return; 367 } 368 369 updateKeyModifiers(e); 370 mousePos = e.getPoint(); 371 372 if (state == State.selecting) { 373 if (targetWay != null) { 374 getCurrentDataSet().setSelected(targetWay.getPrimitiveId()); 375 updateStateByCurrentSelection(); 376 } 377 } else if (state == State.improving && mousePos != null) { 378 // Checking if the new coordinate is outside of the world 379 if (mv.getLatLon(mousePos.x, mousePos.y).isOutSideWorld()) { 380 JOptionPane.showMessageDialog(Main.parent, 381 tr("Cannot place a node outside of the world."), 382 tr("Warning"), JOptionPane.WARNING_MESSAGE); 383 return; 384 } 385 386 if (ctrl && !alt && candidateSegment != null) { 387 // Adding a new node to the highlighted segment 388 // Important: If there are other ways containing the same 389 // segment, a node must added to all of that ways. 390 Collection<Command> virtualCmds = new LinkedList<Command>(); 391 392 // Creating a new node 393 Node virtualNode = new Node(mv.getEastNorth(mousePos.x, 394 mousePos.y)); 395 virtualCmds.add(new AddCommand(virtualNode)); 396 397 // Looking for candidateSegment copies in ways that are 398 // referenced 399 // by candidateSegment nodes 400 List<Way> firstNodeWays = OsmPrimitive.getFilteredList( 401 candidateSegment.getFirstNode().getReferrers(), 402 Way.class); 403 List<Way> secondNodeWays = OsmPrimitive.getFilteredList( 404 candidateSegment.getFirstNode().getReferrers(), 405 Way.class); 406 407 Collection<WaySegment> virtualSegments = new LinkedList<WaySegment>(); 408 for (Way w : firstNodeWays) { 409 List<Pair<Node, Node>> wpps = w.getNodePairs(true); 410 for (Way w2 : secondNodeWays) { 411 if (!w.equals(w2)) { 412 continue; 413 } 414 // A way is referenced in both nodes. 415 // Checking if there is such segment 416 int i = -1; 417 for (Pair<Node, Node> wpp : wpps) { 418 ++i; 419 if ((wpp.a.equals(candidateSegment.getFirstNode()) 420 && wpp.b.equals(candidateSegment.getSecondNode()) || (wpp.b.equals(candidateSegment.getFirstNode()) && wpp.a.equals(candidateSegment.getSecondNode())))) { 421 virtualSegments.add(new WaySegment(w, i)); 422 } 423 } 424 } 425 } 426 427 // Adding the node to all segments found 428 for (WaySegment virtualSegment : virtualSegments) { 429 Way w = virtualSegment.way; 430 Way wnew = new Way(w); 431 wnew.addNode(virtualSegment.lowerIndex + 1, virtualNode); 432 virtualCmds.add(new ChangeCommand(w, wnew)); 433 } 434 435 // Finishing the sequence command 436 String text = trn("Add and a new node to way", 437 "Add and a new node to {0} ways", 438 virtualSegments.size(), virtualSegments.size()); 439 440 Main.main.undoRedo.add(new SequenceCommand(text, virtualCmds)); 441 442 } else if (alt && !ctrl && candidateNode != null) { 443 // Deleting the highlighted node 444 445 //check to see if node has interesting keys 446 Iterator<String> keyIterator = candidateNode.getKeys().keySet().iterator(); 447 boolean hasTags = false; 448 while (keyIterator.hasNext()) { 449 String key = keyIterator.next(); 450 if (!OsmPrimitive.isUninterestingKey(key)) { 451 hasTags = true; 452 break; 453 } 454 } 455 456 //check to see if node is in use by more than one object 457 List<OsmPrimitive> referrers = candidateNode.getReferrers(); 458 List<Way> ways = OsmPrimitive.getFilteredList(referrers, Way.class); 459 if (referrers.size() != 1 || ways.size() != 1) { 460 JOptionPane.showMessageDialog(Main.parent, 461 tr("Cannot delete node that is referenced by multiple objects"), 462 tr("Error"), JOptionPane.ERROR_MESSAGE); 463 } else if (hasTags) { 464 JOptionPane.showMessageDialog(Main.parent, 465 tr("Cannot delete node that has tags"), 466 tr("Error"), JOptionPane.ERROR_MESSAGE); 467 } else { 468 List<Node> nodeList = new ArrayList<Node>(); 469 nodeList.add(candidateNode); 470 Command deleteCmd = DeleteCommand.delete(getEditLayer(), nodeList, true); 471 Main.main.undoRedo.add(deleteCmd); 472 } 473 474 475 } else if (candidateNode != null) { 476 // Moving the highlighted node 477 EastNorth nodeEN = candidateNode.getEastNorth(); 478 EastNorth cursorEN = mv.getEastNorth(mousePos.x, mousePos.y); 479 480 Main.main.undoRedo.add(new MoveCommand(candidateNode, cursorEN.east() - nodeEN.east(), cursorEN.north() 481 - nodeEN.north())); 482 } 483 } 484 485 mousePos = null; 486 updateCursor(); 487 updateStatusLine(); 488 Main.map.mapView.repaint(); 489 } 490 491 @Override 492 public void mouseExited(MouseEvent e) { 493 if (!isEnabled()) { 494 return; 495 } 496 497 if (!dragging) { 498 mousePos = null; 499 } 500 Main.map.mapView.repaint(); 501 } 502 503 // ------------------------------------------------------------------------- 504 // Custom methods 505 // ------------------------------------------------------------------------- 506 /** 507 * Sets new cursor depending on state, mouse position 508 */ 509 private void updateCursor() { 510 if (!isEnabled()) { 511 mv.setNewCursor(null, this); 512 return; 513 } 514 515 if (state == State.selecting) { 516 mv.setNewCursor(targetWay == null ? cursorSelect 517 : cursorSelectHover, this); 518 } else if (state == State.improving) { 519 if (alt && !ctrl) { 520 mv.setNewCursor(cursorImproveDelete, this); 521 } else if (shift || dragging) { 522 if (ctrl) { 523 mv.setNewCursor(cursorImproveAddLock, this); 524 } else { 525 mv.setNewCursor(cursorImproveLock, this); 526 } 527 } else if (ctrl && !alt) { 528 mv.setNewCursor(cursorImproveAdd, this); 529 } else { 530 mv.setNewCursor(cursorImprove, this); 531 } 532 } 533 } 534 535 /** 536 * Updates these objects under cursor: targetWay, candidateNode, 537 * candidateSegment 538 */ 539 public void updateCursorDependentObjectsIfNeeded() { 540 if (state == State.improving && (shift || dragging) 541 && !(candidateNode == null && candidateSegment == null)) { 542 return; 543 } 544 545 if (mousePos == null) { 546 candidateNode = null; 547 candidateSegment = null; 548 return; 549 } 550 551 if (state == State.selecting) { 552 targetWay = ImproveWayAccuracyHelper.findWay(mv, mousePos); 553 } else if (state == State.improving) { 554 if (ctrl && !alt) { 555 candidateSegment = ImproveWayAccuracyHelper.findCandidateSegment(mv, 556 targetWay, mousePos); 557 candidateNode = null; 558 } else { 559 candidateNode = ImproveWayAccuracyHelper.findCandidateNode(mv, 560 targetWay, mousePos); 561 candidateSegment = null; 562 } 563 } 564 } 565 566 /** 567 * Switches to Selecting state 568 */ 569 public void startSelecting() { 570 state = State.selecting; 571 572 targetWay = null; 573 if (getCurrentDataSet() != null) { 574 getCurrentDataSet().clearSelection(); 575 } 576 577 mv.repaint(); 578 updateStatusLine(); 579 } 580 581 /** 582 * Switches to Improving state 583 * 584 * @param targetWay Way that is going to be improved 585 */ 586 public void startImproving(Way targetWay) { 587 state = State.improving; 588 589 Collection<OsmPrimitive> currentSelection = getCurrentDataSet().getSelected(); 590 if (currentSelection.size() != 1 591 || !currentSelection.iterator().next().equals(targetWay)) { 592 selectionChangedBlocked = true; 593 getCurrentDataSet().clearSelection(); 594 getCurrentDataSet().setSelected(targetWay.getPrimitiveId()); 595 selectionChangedBlocked = false; 596 } 597 598 this.targetWay = targetWay; 599 this.candidateNode = null; 600 this.candidateSegment = null; 601 602 mv.repaint(); 603 updateStatusLine(); 604 } 605 606 /** 607 * Updates the state according to the current selection. Goes to Improve 608 * state if a single way or node is selected. Extracts a way by a node in 609 * the second case. 610 * 611 */ 612 private void updateStateByCurrentSelection() { 613 final ArrayList<Node> nodeList = new ArrayList<Node>(); 614 final ArrayList<Way> wayList = new ArrayList<Way>(); 615 final Collection<OsmPrimitive> sel = getCurrentDataSet().getSelected(); 616 617 // Collecting nodes and ways from the selection 618 for (OsmPrimitive p : sel) { 619 if (p instanceof Way) { 620 wayList.add((Way) p); 621 } 622 if (p instanceof Node) { 623 nodeList.add((Node) p); 624 } 625 } 626 627 if (wayList.size() == 1) { 628 // Starting improving the single selected way 629 startImproving(wayList.get(0)); 630 return; 631 } else if (nodeList.size() > 0) { 632 // Starting improving the only way of the single selected node 633 if (nodeList.size() == 1) { 634 List<OsmPrimitive> r = nodeList.get(0).getReferrers(); 635 if (r.size() == 1 && (r.get(0) instanceof Way)) { 636 startImproving((Way) r.get(0)); 637 return; 638 } 639 } 640 } 641 642 // Starting selecting by default 643 startSelecting(); 644 } 645 }