source: josm/trunk/src/org/openstreetmap/josm/actions/mapmode/ImproveWayAccuracyAction.java@ 9519

Last change on this file since 9519 was 9358, checked in by simon04, 8 years ago

fix #12309 - Improve Way Accuracy: draw helper lines to intersecting way

  • Property svn:eol-style set to native
File size: 23.9 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.actions.mapmode;
3
4import static org.openstreetmap.josm.tools.I18n.marktr;
5import static org.openstreetmap.josm.tools.I18n.tr;
6import static org.openstreetmap.josm.tools.I18n.trn;
7
8import java.awt.Color;
9import java.awt.Cursor;
10import java.awt.Graphics2D;
11import java.awt.Point;
12import java.awt.Stroke;
13import java.awt.event.KeyEvent;
14import java.awt.event.MouseEvent;
15import java.awt.geom.GeneralPath;
16import java.util.ArrayList;
17import java.util.Collection;
18import java.util.LinkedList;
19import java.util.List;
20
21import javax.swing.JOptionPane;
22
23import org.openstreetmap.josm.Main;
24import org.openstreetmap.josm.command.AddCommand;
25import org.openstreetmap.josm.command.ChangeCommand;
26import org.openstreetmap.josm.command.Command;
27import org.openstreetmap.josm.command.DeleteCommand;
28import org.openstreetmap.josm.command.MoveCommand;
29import org.openstreetmap.josm.command.SequenceCommand;
30import org.openstreetmap.josm.data.Bounds;
31import org.openstreetmap.josm.data.SelectionChangedListener;
32import org.openstreetmap.josm.data.coor.EastNorth;
33import org.openstreetmap.josm.data.osm.DataSet;
34import org.openstreetmap.josm.data.osm.Node;
35import org.openstreetmap.josm.data.osm.OsmPrimitive;
36import org.openstreetmap.josm.data.osm.Way;
37import org.openstreetmap.josm.data.osm.WaySegment;
38import org.openstreetmap.josm.data.osm.visitor.paint.PaintColors;
39import org.openstreetmap.josm.gui.MapFrame;
40import org.openstreetmap.josm.gui.MapView;
41import org.openstreetmap.josm.gui.layer.Layer;
42import org.openstreetmap.josm.gui.layer.MapViewPaintable;
43import org.openstreetmap.josm.gui.layer.OsmDataLayer;
44import org.openstreetmap.josm.gui.util.GuiHelper;
45import org.openstreetmap.josm.gui.util.ModifierListener;
46import org.openstreetmap.josm.tools.ImageProvider;
47import org.openstreetmap.josm.tools.Pair;
48import org.openstreetmap.josm.tools.Shortcut;
49
50/**
51 * @author Alexander Kachkaev <alexander@kachkaev.ru>, 2011
52 */
53public class ImproveWayAccuracyAction extends MapMode implements MapViewPaintable,
54 SelectionChangedListener, ModifierListener {
55
56 enum State {
57 selecting, improving
58 }
59
60 private State state;
61
62 private MapView mv;
63
64 private static final long serialVersionUID = 42L;
65
66 private transient Way targetWay;
67 private transient Node candidateNode;
68 private transient WaySegment candidateSegment;
69
70 private Point mousePos;
71 private boolean dragging;
72
73 private final Cursor cursorSelect;
74 private final Cursor cursorSelectHover;
75 private final Cursor cursorImprove;
76 private final Cursor cursorImproveAdd;
77 private final Cursor cursorImproveDelete;
78 private final Cursor cursorImproveAddLock;
79 private final Cursor cursorImproveLock;
80
81 private Color guideColor;
82 private transient Stroke selectTargetWayStroke;
83 private transient Stroke moveNodeStroke;
84 private transient Stroke moveNodeIntersectingStroke;
85 private transient Stroke addNodeStroke;
86 private transient Stroke deleteNodeStroke;
87 private int dotSize;
88
89 private boolean selectionChangedBlocked;
90
91 protected String oldModeHelpText;
92
93 /**
94 * Constructs a new {@code ImproveWayAccuracyAction}.
95 * @param mapFrame Map frame
96 */
97 public ImproveWayAccuracyAction(MapFrame mapFrame) {
98 super(tr("Improve Way Accuracy"), "improvewayaccuracy",
99 tr("Improve Way Accuracy mode"),
100 Shortcut.registerShortcut("mapmode:ImproveWayAccuracy",
101 tr("Mode: {0}", tr("Improve Way Accuracy")),
102 KeyEvent.VK_W, Shortcut.DIRECT), mapFrame, Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR));
103
104 cursorSelect = ImageProvider.getCursor("normal", "mode");
105 cursorSelectHover = ImageProvider.getCursor("hand", "mode");
106 cursorImprove = ImageProvider.getCursor("crosshair", null);
107 cursorImproveAdd = ImageProvider.getCursor("crosshair", "addnode");
108 cursorImproveDelete = ImageProvider.getCursor("crosshair", "delete_node");
109 cursorImproveAddLock = ImageProvider.getCursor("crosshair",
110 "add_node_lock");
111 cursorImproveLock = ImageProvider.getCursor("crosshair", "lock");
112 readPreferences();
113 }
114
115 // -------------------------------------------------------------------------
116 // Mode methods
117 // -------------------------------------------------------------------------
118 @Override
119 public void enterMode() {
120 if (!isEnabled()) {
121 return;
122 }
123 super.enterMode();
124 readPreferences();
125
126 mv = Main.map.mapView;
127 mousePos = null;
128 oldModeHelpText = "";
129
130 if (getCurrentDataSet() == null) {
131 return;
132 }
133
134 updateStateByCurrentSelection();
135
136 Main.map.mapView.addMouseListener(this);
137 Main.map.mapView.addMouseMotionListener(this);
138 Main.map.mapView.addTemporaryLayer(this);
139 DataSet.addSelectionListener(this);
140
141 Main.map.keyDetector.addModifierListener(this);
142 }
143
144 private void readPreferences() {
145 guideColor = Main.pref.getColor(marktr("improve way accuracy helper line"), null);
146 if (guideColor == null) guideColor = PaintColors.HIGHLIGHT.get();
147
148 selectTargetWayStroke = GuiHelper.getCustomizedStroke(Main.pref.get("improvewayaccuracy.stroke.select-target", "2"));
149 moveNodeStroke = GuiHelper.getCustomizedStroke(Main.pref.get("improvewayaccuracy.stroke.move-node", "1 6"));
150 moveNodeIntersectingStroke = GuiHelper.getCustomizedStroke(Main.pref.get("improvewayaccuracy.stroke.move-node-intersecting", "1 2 6"));
151 addNodeStroke = GuiHelper.getCustomizedStroke(Main.pref.get("improvewayaccuracy.stroke.add-node", "1"));
152 deleteNodeStroke = GuiHelper.getCustomizedStroke(Main.pref.get("improvewayaccuracy.stroke.delete-node", "1"));
153 dotSize = Main.pref.getInteger("improvewayaccuracy.dot-size", 6);
154 }
155
156 @Override
157 public void exitMode() {
158 super.exitMode();
159
160 Main.map.mapView.removeMouseListener(this);
161 Main.map.mapView.removeMouseMotionListener(this);
162 Main.map.mapView.removeTemporaryLayer(this);
163 DataSet.removeSelectionListener(this);
164
165 Main.map.keyDetector.removeModifierListener(this);
166 Main.map.mapView.repaint();
167 }
168
169 @Override
170 protected void updateStatusLine() {
171 String newModeHelpText = getModeHelpText();
172 if (!newModeHelpText.equals(oldModeHelpText)) {
173 oldModeHelpText = newModeHelpText;
174 Main.map.statusLine.setHelpText(newModeHelpText);
175 Main.map.statusLine.repaint();
176 }
177 }
178
179 @Override
180 public String getModeHelpText() {
181 if (state == State.selecting) {
182 if (targetWay != null) {
183 return tr("Click on the way to start improving its shape.");
184 } else {
185 return tr("Select a way that you want to make more accurate.");
186 }
187 } else {
188 if (ctrl) {
189 return tr("Click to add a new node. Release Ctrl to move existing nodes or hold Alt to delete.");
190 } else if (alt) {
191 return tr("Click to delete the highlighted node. Release Alt to move existing nodes or hold Ctrl to add new nodes.");
192 } else {
193 return tr("Click to move the highlighted node. Hold Ctrl to add new nodes, or Alt to delete.");
194 }
195 }
196 }
197
198 @Override
199 public boolean layerIsSupported(Layer l) {
200 return l instanceof OsmDataLayer;
201 }
202
203 @Override
204 protected void updateEnabledState() {
205 setEnabled(getEditLayer() != null);
206 }
207
208 // -------------------------------------------------------------------------
209 // MapViewPaintable methods
210 // -------------------------------------------------------------------------
211 /**
212 * Redraws temporary layer. Highlights targetWay in select mode. Draws
213 * preview lines in improve mode and highlights the candidateNode
214 */
215 @Override
216 public void paint(Graphics2D g, MapView mv, Bounds bbox) {
217 if (mousePos == null) {
218 return;
219 }
220
221 g.setColor(guideColor);
222
223 if (state == State.selecting && targetWay != null) {
224 // Highlighting the targetWay in Selecting state
225 // Non-native highlighting is used, because sometimes highlighted
226 // segments are covered with others, which is bad.
227 g.setStroke(selectTargetWayStroke);
228
229 List<Node> nodes = targetWay.getNodes();
230
231 GeneralPath b = new GeneralPath();
232 Point p0 = mv.getPoint(nodes.get(0));
233 Point pn;
234 b.moveTo(p0.x, p0.y);
235
236 for (Node n : nodes) {
237 pn = mv.getPoint(n);
238 b.lineTo(pn.x, pn.y);
239 }
240 if (targetWay.isClosed()) {
241 b.lineTo(p0.x, p0.y);
242 }
243
244 g.draw(b);
245
246 } else if (state == State.improving) {
247 // Drawing preview lines and highlighting the node
248 // that is going to be moved.
249 // Non-native highlighting is used here as well.
250
251 // Finding endpoints
252 Point p1 = null, p2 = null;
253 if (ctrl && candidateSegment != null) {
254 g.setStroke(addNodeStroke);
255 p1 = mv.getPoint(candidateSegment.getFirstNode());
256 p2 = mv.getPoint(candidateSegment.getSecondNode());
257 } else if (!alt && !ctrl && candidateNode != null) {
258 g.setStroke(moveNodeStroke);
259 List<Pair<Node, Node>> wpps = targetWay.getNodePairs(false);
260 for (Pair<Node, Node> wpp : wpps) {
261 if (wpp.a == candidateNode) {
262 p1 = mv.getPoint(wpp.b);
263 }
264 if (wpp.b == candidateNode) {
265 p2 = mv.getPoint(wpp.a);
266 }
267 if (p1 != null && p2 != null) {
268 break;
269 }
270 }
271 } else if (alt && !ctrl && candidateNode != null) {
272 g.setStroke(deleteNodeStroke);
273 List<Node> nodes = targetWay.getNodes();
274 int index = nodes.indexOf(candidateNode);
275
276 // Only draw line if node is not first and/or last
277 if (index != 0 && index != (nodes.size() - 1)) {
278 p1 = mv.getPoint(nodes.get(index - 1));
279 p2 = mv.getPoint(nodes.get(index + 1));
280 }
281 // TODO: indicate what part that will be deleted? (for end nodes)
282 }
283
284
285 // Drawing preview lines
286 GeneralPath b = new GeneralPath();
287 if (alt && !ctrl) {
288 // In delete mode
289 if (p1 != null && p2 != null) {
290 b.moveTo(p1.x, p1.y);
291 b.lineTo(p2.x, p2.y);
292 }
293 } else {
294 // In add or move mode
295 if (p1 != null) {
296 b.moveTo(mousePos.x, mousePos.y);
297 b.lineTo(p1.x, p1.y);
298 }
299 if (p2 != null) {
300 b.moveTo(mousePos.x, mousePos.y);
301 b.lineTo(p2.x, p2.y);
302 }
303 }
304 g.draw(b);
305
306 // Highlighting candidateNode
307 if (candidateNode != null) {
308 p1 = mv.getPoint(candidateNode);
309 g.fillRect(p1.x - dotSize/2, p1.y - dotSize/2, dotSize, dotSize);
310 }
311
312 if (!alt && !ctrl && candidateNode != null) {
313 b.reset();
314 drawIntersectingWayHelperLines(mv, b);
315 g.setStroke(moveNodeIntersectingStroke);
316 g.draw(b);
317 }
318
319 }
320 }
321
322 protected void drawIntersectingWayHelperLines(MapView mv, GeneralPath b) {
323 for (final OsmPrimitive referrer : candidateNode.getReferrers()) {
324 if (!(referrer instanceof Way) || targetWay.equals(referrer)) {
325 continue;
326 }
327 final List<Node> nodes = ((Way) referrer).getNodes();
328 for (int i = 0; i < nodes.size(); i++) {
329 if (!candidateNode.equals(nodes.get(i))) {
330 continue;
331 }
332 if (i > 0) {
333 final Point p = mv.getPoint(nodes.get(i - 1));
334 b.moveTo(mousePos.x, mousePos.y);
335 b.lineTo(p.x, p.y);
336 }
337 if (i < nodes.size() - 1) {
338 final Point p = mv.getPoint(nodes.get(i + 1));
339 b.moveTo(mousePos.x, mousePos.y);
340 b.lineTo(p.x, p.y);
341 }
342 }
343 }
344 }
345
346 // -------------------------------------------------------------------------
347 // Event handlers
348 // -------------------------------------------------------------------------
349 @Override
350 public void modifiersChanged(int modifiers) {
351 if (!Main.isDisplayingMapView() || !Main.map.mapView.isActiveLayerDrawable()) {
352 return;
353 }
354 updateKeyModifiers(modifiers);
355 updateCursorDependentObjectsIfNeeded();
356 updateCursor();
357 updateStatusLine();
358 Main.map.mapView.repaint();
359 }
360
361 @Override
362 public void selectionChanged(Collection<? extends OsmPrimitive> newSelection) {
363 if (selectionChangedBlocked) {
364 return;
365 }
366 updateStateByCurrentSelection();
367 }
368
369 @Override
370 public void mouseDragged(MouseEvent e) {
371 dragging = true;
372 mouseMoved(e);
373 }
374
375 @Override
376 public void mouseMoved(MouseEvent e) {
377 if (!isEnabled()) {
378 return;
379 }
380
381 mousePos = e.getPoint();
382
383 updateKeyModifiers(e);
384 updateCursorDependentObjectsIfNeeded();
385 updateCursor();
386 updateStatusLine();
387 Main.map.mapView.repaint();
388 }
389
390 @Override
391 public void mouseReleased(MouseEvent e) {
392 dragging = false;
393 if (!isEnabled() || e.getButton() != MouseEvent.BUTTON1) {
394 return;
395 }
396
397 updateKeyModifiers(e);
398 mousePos = e.getPoint();
399
400 if (state == State.selecting) {
401 if (targetWay != null) {
402 getCurrentDataSet().setSelected(targetWay.getPrimitiveId());
403 updateStateByCurrentSelection();
404 }
405 } else if (state == State.improving && mousePos != null) {
406 // Checking if the new coordinate is outside of the world
407 if (mv.getLatLon(mousePos.x, mousePos.y).isOutSideWorld()) {
408 JOptionPane.showMessageDialog(Main.parent,
409 tr("Cannot add a node outside of the world."),
410 tr("Warning"), JOptionPane.WARNING_MESSAGE);
411 return;
412 }
413
414 if (ctrl && !alt && candidateSegment != null) {
415 // Adding a new node to the highlighted segment
416 // Important: If there are other ways containing the same
417 // segment, a node must added to all of that ways.
418 Collection<Command> virtualCmds = new LinkedList<>();
419
420 // Creating a new node
421 Node virtualNode = new Node(mv.getEastNorth(mousePos.x,
422 mousePos.y));
423 virtualCmds.add(new AddCommand(virtualNode));
424
425 // Looking for candidateSegment copies in ways that are
426 // referenced
427 // by candidateSegment nodes
428 List<Way> firstNodeWays = OsmPrimitive.getFilteredList(
429 candidateSegment.getFirstNode().getReferrers(),
430 Way.class);
431 List<Way> secondNodeWays = OsmPrimitive.getFilteredList(
432 candidateSegment.getFirstNode().getReferrers(),
433 Way.class);
434
435 Collection<WaySegment> virtualSegments = new LinkedList<>();
436 for (Way w : firstNodeWays) {
437 List<Pair<Node, Node>> wpps = w.getNodePairs(true);
438 for (Way w2 : secondNodeWays) {
439 if (!w.equals(w2)) {
440 continue;
441 }
442 // A way is referenced in both nodes.
443 // Checking if there is such segment
444 int i = -1;
445 for (Pair<Node, Node> wpp : wpps) {
446 ++i;
447 boolean ab = wpp.a.equals(candidateSegment.getFirstNode())
448 && wpp.b.equals(candidateSegment.getSecondNode());
449 boolean ba = wpp.b.equals(candidateSegment.getFirstNode())
450 && wpp.a.equals(candidateSegment.getSecondNode());
451 if (ab || ba) {
452 virtualSegments.add(new WaySegment(w, i));
453 }
454 }
455 }
456 }
457
458 // Adding the node to all segments found
459 for (WaySegment virtualSegment : virtualSegments) {
460 Way w = virtualSegment.way;
461 Way wnew = new Way(w);
462 wnew.addNode(virtualSegment.lowerIndex + 1, virtualNode);
463 virtualCmds.add(new ChangeCommand(w, wnew));
464 }
465
466 // Finishing the sequence command
467 String text = trn("Add a new node to way",
468 "Add a new node to {0} ways",
469 virtualSegments.size(), virtualSegments.size());
470
471 Main.main.undoRedo.add(new SequenceCommand(text, virtualCmds));
472
473 } else if (alt && !ctrl && candidateNode != null) {
474 // Deleting the highlighted node
475
476 //check to see if node is in use by more than one object
477 List<OsmPrimitive> referrers = candidateNode.getReferrers();
478 List<Way> ways = OsmPrimitive.getFilteredList(referrers, Way.class);
479 if (referrers.size() != 1 || ways.size() != 1) {
480 // detach node from way
481 final Way newWay = new Way(targetWay);
482 final List<Node> nodes = newWay.getNodes();
483 nodes.remove(candidateNode);
484 newWay.setNodes(nodes);
485 Main.main.undoRedo.add(new ChangeCommand(targetWay, newWay));
486 } else if (candidateNode.isTagged()) {
487 JOptionPane.showMessageDialog(Main.parent,
488 tr("Cannot delete node that has tags"),
489 tr("Error"), JOptionPane.ERROR_MESSAGE);
490 } else {
491 List<Node> nodeList = new ArrayList<>();
492 nodeList.add(candidateNode);
493 Command deleteCmd = DeleteCommand.delete(getEditLayer(), nodeList, true);
494 if (deleteCmd != null) {
495 Main.main.undoRedo.add(deleteCmd);
496 }
497 }
498
499
500 } else if (candidateNode != null) {
501 // Moving the highlighted node
502 EastNorth nodeEN = candidateNode.getEastNorth();
503 EastNorth cursorEN = mv.getEastNorth(mousePos.x, mousePos.y);
504
505 Main.main.undoRedo.add(new MoveCommand(candidateNode, cursorEN.east() - nodeEN.east(), cursorEN.north()
506 - nodeEN.north()));
507 }
508 }
509
510 mousePos = null;
511 updateCursor();
512 updateStatusLine();
513 Main.map.mapView.repaint();
514 }
515
516 @Override
517 public void mouseExited(MouseEvent e) {
518 if (!isEnabled()) {
519 return;
520 }
521
522 if (!dragging) {
523 mousePos = null;
524 }
525 Main.map.mapView.repaint();
526 }
527
528 // -------------------------------------------------------------------------
529 // Custom methods
530 // -------------------------------------------------------------------------
531 /**
532 * Sets new cursor depending on state, mouse position
533 */
534 private void updateCursor() {
535 if (!isEnabled()) {
536 mv.setNewCursor(null, this);
537 return;
538 }
539
540 if (state == State.selecting) {
541 mv.setNewCursor(targetWay == null ? cursorSelect
542 : cursorSelectHover, this);
543 } else if (state == State.improving) {
544 if (alt && !ctrl) {
545 mv.setNewCursor(cursorImproveDelete, this);
546 } else if (shift || dragging) {
547 if (ctrl) {
548 mv.setNewCursor(cursorImproveAddLock, this);
549 } else {
550 mv.setNewCursor(cursorImproveLock, this);
551 }
552 } else if (ctrl && !alt) {
553 mv.setNewCursor(cursorImproveAdd, this);
554 } else {
555 mv.setNewCursor(cursorImprove, this);
556 }
557 }
558 }
559
560 /**
561 * Updates these objects under cursor: targetWay, candidateNode,
562 * candidateSegment
563 */
564 public void updateCursorDependentObjectsIfNeeded() {
565 if (state == State.improving && (shift || dragging)
566 && !(candidateNode == null && candidateSegment == null)) {
567 return;
568 }
569
570 if (mousePos == null) {
571 candidateNode = null;
572 candidateSegment = null;
573 return;
574 }
575
576 if (state == State.selecting) {
577 targetWay = ImproveWayAccuracyHelper.findWay(mv, mousePos);
578 } else if (state == State.improving) {
579 if (ctrl && !alt) {
580 candidateSegment = ImproveWayAccuracyHelper.findCandidateSegment(mv,
581 targetWay, mousePos);
582 candidateNode = null;
583 } else {
584 candidateNode = ImproveWayAccuracyHelper.findCandidateNode(mv,
585 targetWay, mousePos);
586 candidateSegment = null;
587 }
588 }
589 }
590
591 /**
592 * Switches to Selecting state
593 */
594 public void startSelecting() {
595 state = State.selecting;
596
597 targetWay = null;
598
599 mv.repaint();
600 updateStatusLine();
601 }
602
603 /**
604 * Switches to Improving state
605 *
606 * @param targetWay Way that is going to be improved
607 */
608 public void startImproving(Way targetWay) {
609 state = State.improving;
610
611 Collection<OsmPrimitive> currentSelection = getCurrentDataSet().getSelected();
612 if (currentSelection.size() != 1
613 || !currentSelection.iterator().next().equals(targetWay)) {
614 selectionChangedBlocked = true;
615 getCurrentDataSet().clearSelection();
616 getCurrentDataSet().setSelected(targetWay.getPrimitiveId());
617 selectionChangedBlocked = false;
618 }
619
620 this.targetWay = targetWay;
621 this.candidateNode = null;
622 this.candidateSegment = null;
623
624 mv.repaint();
625 updateStatusLine();
626 }
627
628 /**
629 * Updates the state according to the current selection. Goes to Improve
630 * state if a single way or node is selected. Extracts a way by a node in
631 * the second case.
632 *
633 */
634 private void updateStateByCurrentSelection() {
635 final List<Node> nodeList = new ArrayList<>();
636 final List<Way> wayList = new ArrayList<>();
637 final Collection<OsmPrimitive> sel = getCurrentDataSet().getSelected();
638
639 // Collecting nodes and ways from the selection
640 for (OsmPrimitive p : sel) {
641 if (p instanceof Way) {
642 wayList.add((Way) p);
643 }
644 if (p instanceof Node) {
645 nodeList.add((Node) p);
646 }
647 }
648
649 if (wayList.size() == 1) {
650 // Starting improving the single selected way
651 startImproving(wayList.get(0));
652 return;
653 } else if (nodeList.size() == 1) {
654 // Starting improving the only way of the single selected node
655 List<OsmPrimitive> r = nodeList.get(0).getReferrers();
656 if (r.size() == 1 && (r.get(0) instanceof Way)) {
657 startImproving((Way) r.get(0));
658 return;
659 }
660 }
661
662 // Starting selecting by default
663 startSelecting();
664 }
665}
Note: See TracBrowser for help on using the repository browser.