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

Last change on this file since 12581 was 12581, checked in by bastiK, 7 years ago

see #14794 - javadoc

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