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