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

Last change on this file since 12517 was 12517, checked in by Don-vip, 7 years ago

see #11924 - use extended event modifiers, deprecate old methods - see https://bugs.openjdk.java.net/browse/JDK-8143077

  • Property svn:eol-style set to native
File size: 24.5 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 * @author Alexander Kachkaev <alexander@kachkaev.ru>, 2011
55 */
56public class ImproveWayAccuracyAction extends MapMode implements
57 SelectionChangedListener, ModifierExListener {
58
59 private static final String CROSSHAIR = "crosshair";
60
61 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 * @since 11713
115 */
116 public ImproveWayAccuracyAction() {
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), 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.addModifierExListener(this);
153 }
154
155 @Override
156 protected void readPreferences() {
157 guideColor = new ColorProperty(marktr("improve way accuracy helper line"), Color.RED).get();
158 }
159
160 @Override
161 public void exitMode() {
162 super.exitMode();
163
164 Main.map.mapView.removeMouseListener(this);
165 Main.map.mapView.removeMouseMotionListener(this);
166 Main.map.mapView.removeTemporaryLayer(temporaryLayer);
167 DataSet.removeSelectionListener(this);
168
169 Main.map.keyDetector.removeModifierExListener(this);
170 temporaryLayer.invalidate();
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(getLayerManager().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 * @param g The graphics
219 * @param mv The map view
220 * @param bbox The bounding box
221 */
222 public void paint(Graphics2D g, MapView mv, Bounds bbox) {
223 if (mousePos == null) {
224 return;
225 }
226
227 g.setColor(guideColor);
228
229 if (state == State.SELECTING && targetWay != null) {
230 // Highlighting the targetWay in Selecting state
231 // Non-native highlighting is used, because sometimes highlighted
232 // segments are covered with others, which is bad.
233 BasicStroke stroke = SELECT_TARGET_WAY_STROKE.get();
234 g.setStroke(stroke);
235
236 List<Node> nodes = targetWay.getNodes();
237
238 g.draw(new MapViewPath(mv).append(nodes, false).computeClippedLine(stroke));
239
240 } else if (state == State.IMPROVING) {
241 // Drawing preview lines and highlighting the node
242 // that is going to be moved.
243 // Non-native highlighting is used here as well.
244
245 // Finding endpoints
246 Node p1 = null;
247 Node p2 = null;
248 if (ctrl && candidateSegment != null) {
249 g.setStroke(ADD_NODE_STROKE.get());
250 p1 = candidateSegment.getFirstNode();
251 p2 = candidateSegment.getSecondNode();
252 } else if (!alt && !ctrl && candidateNode != null) {
253 g.setStroke(MOVE_NODE_STROKE.get());
254 List<Pair<Node, Node>> wpps = targetWay.getNodePairs(false);
255 for (Pair<Node, Node> wpp : wpps) {
256 if (wpp.a == candidateNode) {
257 p1 = wpp.b;
258 }
259 if (wpp.b == candidateNode) {
260 p2 = wpp.a;
261 }
262 if (p1 != null && p2 != null) {
263 break;
264 }
265 }
266 } else if (alt && !ctrl && candidateNode != null) {
267 g.setStroke(DELETE_NODE_STROKE.get());
268 List<Node> nodes = targetWay.getNodes();
269 int index = nodes.indexOf(candidateNode);
270
271 // Only draw line if node is not first and/or last
272 if (index != 0 && index != (nodes.size() - 1)) {
273 p1 = nodes.get(index - 1);
274 p2 = nodes.get(index + 1);
275 } else if (targetWay.isClosed()) {
276 p1 = targetWay.getNode(1);
277 p2 = targetWay.getNode(nodes.size() - 2);
278 }
279 // TODO: indicate what part that will be deleted? (for end nodes)
280 }
281
282
283 // Drawing preview lines
284 MapViewPath b = new MapViewPath(mv);
285 if (alt && !ctrl) {
286 // In delete mode
287 if (p1 != null && p2 != null) {
288 b.moveTo(p1);
289 b.lineTo(p2);
290 }
291 } else {
292 // In add or move mode
293 if (p1 != null) {
294 b.moveTo(mousePos.x, mousePos.y);
295 b.lineTo(p1);
296 }
297 if (p2 != null) {
298 b.moveTo(mousePos.x, mousePos.y);
299 b.lineTo(p2);
300 }
301 }
302 g.draw(b.computeClippedLine(g.getStroke()));
303
304 // Highlighting candidateNode
305 if (candidateNode != null) {
306 p1 = candidateNode;
307 g.fill(new MapViewPath(mv).shapeAround(p1, SymbolShape.SQUARE, DOT_SIZE.get()));
308 }
309
310 if (!alt && !ctrl && candidateNode != null) {
311 b.reset();
312 drawIntersectingWayHelperLines(mv, b);
313 g.setStroke(MOVE_NODE_INTERSECTING_STROKE.get());
314 g.draw(b.computeClippedLine(g.getStroke()));
315 }
316
317 }
318 }
319
320 protected void drawIntersectingWayHelperLines(MapView mv, MapViewPath b) {
321 for (final OsmPrimitive referrer : candidateNode.getReferrers()) {
322 if (!(referrer instanceof Way) || targetWay.equals(referrer)) {
323 continue;
324 }
325 final List<Node> nodes = ((Way) referrer).getNodes();
326 for (int i = 0; i < nodes.size(); i++) {
327 if (!candidateNode.equals(nodes.get(i))) {
328 continue;
329 }
330 if (i > 0) {
331 b.moveTo(mousePos.x, mousePos.y);
332 b.lineTo(nodes.get(i - 1));
333 }
334 if (i < nodes.size() - 1) {
335 b.moveTo(mousePos.x, mousePos.y);
336 b.lineTo(nodes.get(i + 1));
337 }
338 }
339 }
340 }
341
342 // -------------------------------------------------------------------------
343 // Event handlers
344 // -------------------------------------------------------------------------
345 @Override
346 public void modifiersExChanged(int modifiers) {
347 if (!Main.isDisplayingMapView() || !Main.map.mapView.isActiveLayerDrawable()) {
348 return;
349 }
350 updateKeyModifiersEx(modifiers);
351 updateCursorDependentObjectsIfNeeded();
352 updateCursor();
353 updateStatusLine();
354 temporaryLayer.invalidate();
355 }
356
357 @Override
358 public void selectionChanged(Collection<? extends OsmPrimitive> newSelection) {
359 if (selectionChangedBlocked) {
360 return;
361 }
362 updateStateByCurrentSelection();
363 }
364
365 @Override
366 public void mouseDragged(MouseEvent e) {
367 dragging = true;
368 mouseMoved(e);
369 }
370
371 @Override
372 public void mouseMoved(MouseEvent e) {
373 if (!isEnabled()) {
374 return;
375 }
376
377 mousePos = e.getPoint();
378
379 updateKeyModifiers(e);
380 updateCursorDependentObjectsIfNeeded();
381 updateCursor();
382 updateStatusLine();
383 temporaryLayer.invalidate();
384 }
385
386 @Override
387 public void mouseReleased(MouseEvent e) {
388 dragging = false;
389 if (!isEnabled() || e.getButton() != MouseEvent.BUTTON1) {
390 return;
391 }
392
393 updateKeyModifiers(e);
394 mousePos = e.getPoint();
395
396 if (state == State.SELECTING) {
397 if (targetWay != null) {
398 getLayerManager().getEditDataSet().setSelected(targetWay.getPrimitiveId());
399 updateStateByCurrentSelection();
400 }
401 } else if (state == State.IMPROVING) {
402 // Checking if the new coordinate is outside of the world
403 if (mv.getLatLon(mousePos.x, mousePos.y).isOutSideWorld()) {
404 JOptionPane.showMessageDialog(Main.parent,
405 tr("Cannot add a node outside of the world."),
406 tr("Warning"), JOptionPane.WARNING_MESSAGE);
407 return;
408 }
409
410 if (ctrl && !alt && candidateSegment != null) {
411 // Adding a new node to the highlighted segment
412 // Important: If there are other ways containing the same
413 // segment, a node must added to all of that ways.
414 Collection<Command> virtualCmds = new LinkedList<>();
415
416 // Creating a new node
417 Node virtualNode = new Node(mv.getEastNorth(mousePos.x,
418 mousePos.y));
419 virtualCmds.add(new AddCommand(virtualNode));
420
421 // Looking for candidateSegment copies in ways that are
422 // referenced
423 // by candidateSegment nodes
424 List<Way> firstNodeWays = OsmPrimitive.getFilteredList(
425 candidateSegment.getFirstNode().getReferrers(),
426 Way.class);
427 List<Way> secondNodeWays = OsmPrimitive.getFilteredList(
428 candidateSegment.getFirstNode().getReferrers(),
429 Way.class);
430
431 Collection<WaySegment> virtualSegments = new LinkedList<>();
432 for (Way w : firstNodeWays) {
433 List<Pair<Node, Node>> wpps = w.getNodePairs(true);
434 for (Way w2 : secondNodeWays) {
435 if (!w.equals(w2)) {
436 continue;
437 }
438 // A way is referenced in both nodes.
439 // Checking if there is such segment
440 int i = -1;
441 for (Pair<Node, Node> wpp : wpps) {
442 ++i;
443 boolean ab = wpp.a.equals(candidateSegment.getFirstNode())
444 && wpp.b.equals(candidateSegment.getSecondNode());
445 boolean ba = wpp.b.equals(candidateSegment.getFirstNode())
446 && wpp.a.equals(candidateSegment.getSecondNode());
447 if (ab || ba) {
448 virtualSegments.add(new WaySegment(w, i));
449 }
450 }
451 }
452 }
453
454 // Adding the node to all segments found
455 for (WaySegment virtualSegment : virtualSegments) {
456 Way w = virtualSegment.way;
457 Way wnew = new Way(w);
458 wnew.addNode(virtualSegment.lowerIndex + 1, virtualNode);
459 virtualCmds.add(new ChangeCommand(w, wnew));
460 }
461
462 // Finishing the sequence command
463 String text = trn("Add a new node to way",
464 "Add a new node to {0} ways",
465 virtualSegments.size(), virtualSegments.size());
466
467 Main.main.undoRedo.add(new SequenceCommand(text, virtualCmds));
468
469 } else if (alt && !ctrl && candidateNode != null) {
470 // Deleting the highlighted node
471
472 //check to see if node is in use by more than one object
473 List<OsmPrimitive> referrers = candidateNode.getReferrers();
474 List<Way> ways = OsmPrimitive.getFilteredList(referrers, Way.class);
475 if (referrers.size() != 1 || ways.size() != 1) {
476 // detach node from way
477 final Way newWay = new Way(targetWay);
478 final List<Node> nodes = newWay.getNodes();
479 nodes.remove(candidateNode);
480 newWay.setNodes(nodes);
481 if (nodes.size() < 2) {
482 final Command deleteCmd = DeleteCommand.delete(getLayerManager().getEditLayer(), Collections.singleton(targetWay), true);
483 if (deleteCmd != null) {
484 Main.main.undoRedo.add(deleteCmd);
485 }
486 } else {
487 Main.main.undoRedo.add(new ChangeCommand(targetWay, newWay));
488 }
489 } else if (candidateNode.isTagged()) {
490 JOptionPane.showMessageDialog(Main.parent,
491 tr("Cannot delete node that has tags"),
492 tr("Error"), JOptionPane.ERROR_MESSAGE);
493 } else {
494 final Command deleteCmd = DeleteCommand.delete(getLayerManager().getEditLayer(), Collections.singleton(candidateNode), true);
495 if (deleteCmd != null) {
496 Main.main.undoRedo.add(deleteCmd);
497 }
498 }
499
500
501 } else if (candidateNode != null) {
502 // Moving the highlighted node
503 EastNorth nodeEN = candidateNode.getEastNorth();
504 EastNorth cursorEN = mv.getEastNorth(mousePos.x, mousePos.y);
505
506 Main.main.undoRedo.add(new MoveCommand(candidateNode, cursorEN.east() - nodeEN.east(), cursorEN.north() - nodeEN.north()));
507 }
508 }
509
510 mousePos = null;
511 updateCursor();
512 updateStatusLine();
513 temporaryLayer.invalidate();
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 temporaryLayer.invalidate();
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 temporaryLayer.invalidate();
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 DataSet ds = getLayerManager().getEditDataSet();
612 Collection<OsmPrimitive> currentSelection = ds.getSelected();
613 if (currentSelection.size() != 1
614 || !currentSelection.iterator().next().equals(targetWay)) {
615 selectionChangedBlocked = true;
616 ds.clearSelection();
617 ds.setSelected(targetWay.getPrimitiveId());
618 selectionChangedBlocked = false;
619 }
620
621 this.targetWay = targetWay;
622 this.candidateNode = null;
623 this.candidateSegment = null;
624
625 temporaryLayer.invalidate();
626 updateStatusLine();
627 }
628
629 /**
630 * Updates the state according to the current selection. Goes to Improve
631 * state if a single way or node is selected. Extracts a way by a node in
632 * the second case.
633 *
634 */
635 private void updateStateByCurrentSelection() {
636 final List<Node> nodeList = new ArrayList<>();
637 final List<Way> wayList = new ArrayList<>();
638 final Collection<OsmPrimitive> sel = getLayerManager().getEditDataSet().getSelected();
639
640 // Collecting nodes and ways from the selection
641 for (OsmPrimitive p : sel) {
642 if (p instanceof Way) {
643 wayList.add((Way) p);
644 }
645 if (p instanceof Node) {
646 nodeList.add((Node) p);
647 }
648 }
649
650 if (wayList.size() == 1) {
651 // Starting improving the single selected way
652 startImproving(wayList.get(0));
653 return;
654 } else if (nodeList.size() == 1) {
655 // Starting improving the only way of the single selected node
656 List<OsmPrimitive> r = nodeList.get(0).getReferrers();
657 if (r.size() == 1 && (r.get(0) instanceof Way)) {
658 startImproving((Way) r.get(0));
659 return;
660 }
661 }
662
663 // Starting selecting by default
664 startSelecting();
665 }
666}
Note: See TracBrowser for help on using the repository browser.