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