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

Last change on this file since 11216 was 11144, checked in by michael2402, 8 years ago

Fix #13793: Clip paths in a way that let's the dashes stay in place.

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