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

Last change on this file since 11063 was 10875, checked in by Don-vip, 8 years ago

fix #13413 - Clean ImproveWayAccuracyAction, add new class MapViewPath (patch by michael2402, modified) - gsoc-core

  • Property svn:eol-style set to native
File size: 24.2 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.LinkedList;
18import java.util.List;
19
20import javax.swing.JOptionPane;
21
22import org.openstreetmap.josm.Main;
23import org.openstreetmap.josm.command.AddCommand;
24import org.openstreetmap.josm.command.ChangeCommand;
25import org.openstreetmap.josm.command.Command;
26import org.openstreetmap.josm.command.DeleteCommand;
27import org.openstreetmap.josm.command.MoveCommand;
28import org.openstreetmap.josm.command.SequenceCommand;
29import org.openstreetmap.josm.data.Bounds;
30import org.openstreetmap.josm.data.SelectionChangedListener;
31import org.openstreetmap.josm.data.coor.EastNorth;
32import org.openstreetmap.josm.data.osm.DataSet;
33import org.openstreetmap.josm.data.osm.Node;
34import org.openstreetmap.josm.data.osm.OsmPrimitive;
35import org.openstreetmap.josm.data.osm.Way;
36import org.openstreetmap.josm.data.osm.WaySegment;
37import org.openstreetmap.josm.data.osm.visitor.paint.PaintColors;
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.MapFrame;
43import org.openstreetmap.josm.gui.MapView;
44import org.openstreetmap.josm.gui.draw.MapViewPath;
45import org.openstreetmap.josm.gui.draw.SymbolShape;
46import org.openstreetmap.josm.gui.layer.AbstractMapViewPaintable;
47import org.openstreetmap.josm.gui.layer.Layer;
48import org.openstreetmap.josm.gui.layer.OsmDataLayer;
49import org.openstreetmap.josm.gui.util.ModifierListener;
50import org.openstreetmap.josm.tools.ImageProvider;
51import org.openstreetmap.josm.tools.Pair;
52import org.openstreetmap.josm.tools.Shortcut;
53
54/**
55 * @author Alexander Kachkaev <alexander@kachkaev.ru>, 2011
56 */
57public class ImproveWayAccuracyAction extends MapMode implements
58 SelectionChangedListener, ModifierListener {
59
60 private enum State {
61 SELECTING, IMPROVING
62 }
63
64 private State state;
65
66 private MapView mv;
67
68 private static final long serialVersionUID = 42L;
69
70 private transient Way targetWay;
71 private transient Node candidateNode;
72 private transient WaySegment candidateSegment;
73
74 private Point mousePos;
75 private boolean dragging;
76
77 private final Cursor cursorSelect = ImageProvider.getCursor("normal", "mode");
78 private final Cursor cursorSelectHover = ImageProvider.getCursor("hand", "mode");
79 private final Cursor cursorImprove = ImageProvider.getCursor("crosshair", null);
80 private final Cursor cursorImproveAdd = ImageProvider.getCursor("crosshair", "addnode");
81 private final Cursor cursorImproveDelete = ImageProvider.getCursor("crosshair", "delete_node");
82 private final Cursor cursorImproveAddLock = ImageProvider.getCursor("crosshair", "add_node_lock");
83 private final Cursor cursorImproveLock = ImageProvider.getCursor("crosshair", "lock");
84
85 private Color guideColor;
86
87 private static final CachingProperty<BasicStroke> SELECT_TARGET_WAY_STROKE
88 = new StrokeProperty("improvewayaccuracy.stroke.select-target", "2").cached();
89 private static final CachingProperty<BasicStroke> MOVE_NODE_STROKE
90 = new StrokeProperty("improvewayaccuracy.stroke.move-node", "1 6").cached();
91 private static final CachingProperty<BasicStroke> MOVE_NODE_INTERSECTING_STROKE
92 = new StrokeProperty("improvewayaccuracy.stroke.move-node-intersecting", "1 2 6").cached();
93 private static final CachingProperty<BasicStroke> ADD_NODE_STROKE
94 = new StrokeProperty("improvewayaccuracy.stroke.add-node", "1").cached();
95 private static final CachingProperty<BasicStroke> DELETE_NODE_STROKE
96 = new StrokeProperty("improvewayaccuracy.stroke.delete-node", "1").cached();
97 private static final CachingProperty<Integer> DOT_SIZE
98 = new IntegerProperty("improvewayaccuracy.dot-size", 6).cached();
99
100 private boolean selectionChangedBlocked;
101
102 protected String oldModeHelpText;
103
104 private final transient AbstractMapViewPaintable temporaryLayer = new AbstractMapViewPaintable() {
105 @Override
106 public void paint(Graphics2D g, MapView mv, Bounds bbox) {
107 ImproveWayAccuracyAction.this.paint(g, mv, bbox);
108 }
109 };
110
111 /**
112 * Constructs a new {@code ImproveWayAccuracyAction}.
113 * @param mapFrame Map frame
114 */
115 public ImproveWayAccuracyAction(MapFrame mapFrame) {
116 super(tr("Improve Way Accuracy"), "improvewayaccuracy",
117 tr("Improve Way Accuracy mode"),
118 Shortcut.registerShortcut("mapmode:ImproveWayAccuracy",
119 tr("Mode: {0}", tr("Improve Way Accuracy")),
120 KeyEvent.VK_W, Shortcut.DIRECT), mapFrame, Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR));
121
122 readPreferences();
123 }
124
125 // -------------------------------------------------------------------------
126 // Mode methods
127 // -------------------------------------------------------------------------
128 @Override
129 public void enterMode() {
130 if (!isEnabled()) {
131 return;
132 }
133 super.enterMode();
134 readPreferences();
135
136 mv = Main.map.mapView;
137 mousePos = null;
138 oldModeHelpText = "";
139
140 if (getLayerManager().getEditDataSet() == null) {
141 return;
142 }
143
144 updateStateByCurrentSelection();
145
146 Main.map.mapView.addMouseListener(this);
147 Main.map.mapView.addMouseMotionListener(this);
148 Main.map.mapView.addTemporaryLayer(temporaryLayer);
149 DataSet.addSelectionListener(this);
150
151 Main.map.keyDetector.addModifierListener(this);
152 }
153
154 @Override
155 protected void readPreferences() {
156 guideColor = new ColorProperty(marktr("improve way accuracy helper line"), (Color) null).get();
157 if (guideColor == null)
158 guideColor = PaintColors.HIGHLIGHT.get();
159 }
160
161 @Override
162 public void exitMode() {
163 super.exitMode();
164
165 Main.map.mapView.removeMouseListener(this);
166 Main.map.mapView.removeMouseMotionListener(this);
167 Main.map.mapView.removeTemporaryLayer(temporaryLayer);
168 DataSet.removeSelectionListener(this);
169
170 Main.map.keyDetector.removeModifierListener(this);
171 temporaryLayer.invalidate();
172 }
173
174 @Override
175 protected void updateStatusLine() {
176 String newModeHelpText = getModeHelpText();
177 if (!newModeHelpText.equals(oldModeHelpText)) {
178 oldModeHelpText = newModeHelpText;
179 Main.map.statusLine.setHelpText(newModeHelpText);
180 Main.map.statusLine.repaint();
181 }
182 }
183
184 @Override
185 public String getModeHelpText() {
186 if (state == State.SELECTING) {
187 if (targetWay != null) {
188 return tr("Click on the way to start improving its shape.");
189 } else {
190 return tr("Select a way that you want to make more accurate.");
191 }
192 } else {
193 if (ctrl) {
194 return tr("Click to add a new node. Release Ctrl to move existing nodes or hold Alt to delete.");
195 } else if (alt) {
196 return tr("Click to delete the highlighted node. Release Alt to move existing nodes or hold Ctrl to add new nodes.");
197 } else {
198 return tr("Click to move the highlighted node. Hold Ctrl to add new nodes, or Alt to delete.");
199 }
200 }
201 }
202
203 @Override
204 public boolean layerIsSupported(Layer l) {
205 return l instanceof OsmDataLayer;
206 }
207
208 @Override
209 protected void updateEnabledState() {
210 setEnabled(getLayerManager().getEditLayer() != null);
211 }
212
213 // -------------------------------------------------------------------------
214 // MapViewPaintable methods
215 // -------------------------------------------------------------------------
216 /**
217 * Redraws temporary layer. Highlights targetWay in select mode. Draws
218 * preview lines in improve mode and highlights the candidateNode
219 * @param g The graphics
220 * @param mv The map view
221 * @param bbox The bounding box
222 */
223 public void paint(Graphics2D g, MapView mv, Bounds bbox) {
224 if (mousePos == null) {
225 return;
226 }
227
228 g.setColor(guideColor);
229
230 if (state == State.SELECTING && targetWay != null) {
231 // Highlighting the targetWay in Selecting state
232 // Non-native highlighting is used, because sometimes highlighted
233 // segments are covered with others, which is bad.
234 g.setStroke(SELECT_TARGET_WAY_STROKE.get());
235
236 List<Node> nodes = targetWay.getNodes();
237
238 g.draw(new MapViewPath(mv).append(nodes, false));
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);
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);
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 modifiersChanged(int modifiers) {
347 if (!Main.isDisplayingMapView() || !Main.map.mapView.isActiveLayerDrawable()) {
348 return;
349 }
350 updateKeyModifiers(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 && mousePos != null) {
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 Main.main.undoRedo.add(new ChangeCommand(targetWay, newWay));
482 } else if (candidateNode.isTagged()) {
483 JOptionPane.showMessageDialog(Main.parent,
484 tr("Cannot delete node that has tags"),
485 tr("Error"), JOptionPane.ERROR_MESSAGE);
486 } else {
487 List<Node> nodeList = new ArrayList<>();
488 nodeList.add(candidateNode);
489 Command deleteCmd = DeleteCommand.delete(getLayerManager().getEditLayer(), nodeList, true);
490 if (deleteCmd != null) {
491 Main.main.undoRedo.add(deleteCmd);
492 }
493 }
494
495
496 } else if (candidateNode != null) {
497 // Moving the highlighted node
498 EastNorth nodeEN = candidateNode.getEastNorth();
499 EastNorth cursorEN = mv.getEastNorth(mousePos.x, mousePos.y);
500
501 Main.main.undoRedo.add(new MoveCommand(candidateNode, cursorEN.east() - nodeEN.east(), cursorEN.north() - nodeEN.north()));
502 }
503 }
504
505 mousePos = null;
506 updateCursor();
507 updateStatusLine();
508 temporaryLayer.invalidate();
509 }
510
511 @Override
512 public void mouseExited(MouseEvent e) {
513 if (!isEnabled()) {
514 return;
515 }
516
517 if (!dragging) {
518 mousePos = null;
519 }
520 temporaryLayer.invalidate();
521 }
522
523 // -------------------------------------------------------------------------
524 // Custom methods
525 // -------------------------------------------------------------------------
526 /**
527 * Sets new cursor depending on state, mouse position
528 */
529 private void updateCursor() {
530 if (!isEnabled()) {
531 mv.setNewCursor(null, this);
532 return;
533 }
534
535 if (state == State.SELECTING) {
536 mv.setNewCursor(targetWay == null ? cursorSelect
537 : cursorSelectHover, this);
538 } else if (state == State.IMPROVING) {
539 if (alt && !ctrl) {
540 mv.setNewCursor(cursorImproveDelete, this);
541 } else if (shift || dragging) {
542 if (ctrl) {
543 mv.setNewCursor(cursorImproveAddLock, this);
544 } else {
545 mv.setNewCursor(cursorImproveLock, this);
546 }
547 } else if (ctrl && !alt) {
548 mv.setNewCursor(cursorImproveAdd, this);
549 } else {
550 mv.setNewCursor(cursorImprove, this);
551 }
552 }
553 }
554
555 /**
556 * Updates these objects under cursor: targetWay, candidateNode,
557 * candidateSegment
558 */
559 public void updateCursorDependentObjectsIfNeeded() {
560 if (state == State.IMPROVING && (shift || dragging)
561 && !(candidateNode == null && candidateSegment == null)) {
562 return;
563 }
564
565 if (mousePos == null) {
566 candidateNode = null;
567 candidateSegment = null;
568 return;
569 }
570
571 if (state == State.SELECTING) {
572 targetWay = ImproveWayAccuracyHelper.findWay(mv, mousePos);
573 } else if (state == State.IMPROVING) {
574 if (ctrl && !alt) {
575 candidateSegment = ImproveWayAccuracyHelper.findCandidateSegment(mv,
576 targetWay, mousePos);
577 candidateNode = null;
578 } else {
579 candidateNode = ImproveWayAccuracyHelper.findCandidateNode(mv,
580 targetWay, mousePos);
581 candidateSegment = null;
582 }
583 }
584 }
585
586 /**
587 * Switches to Selecting state
588 */
589 public void startSelecting() {
590 state = State.SELECTING;
591
592 targetWay = null;
593
594 temporaryLayer.invalidate();
595 updateStatusLine();
596 }
597
598 /**
599 * Switches to Improving state
600 *
601 * @param targetWay Way that is going to be improved
602 */
603 public void startImproving(Way targetWay) {
604 state = State.IMPROVING;
605
606 DataSet ds = getLayerManager().getEditDataSet();
607 Collection<OsmPrimitive> currentSelection = ds.getSelected();
608 if (currentSelection.size() != 1
609 || !currentSelection.iterator().next().equals(targetWay)) {
610 selectionChangedBlocked = true;
611 ds.clearSelection();
612 ds.setSelected(targetWay.getPrimitiveId());
613 selectionChangedBlocked = false;
614 }
615
616 this.targetWay = targetWay;
617 this.candidateNode = null;
618 this.candidateSegment = null;
619
620 temporaryLayer.invalidate();
621 updateStatusLine();
622 }
623
624 /**
625 * Updates the state according to the current selection. Goes to Improve
626 * state if a single way or node is selected. Extracts a way by a node in
627 * the second case.
628 *
629 */
630 private void updateStateByCurrentSelection() {
631 final List<Node> nodeList = new ArrayList<>();
632 final List<Way> wayList = new ArrayList<>();
633 final Collection<OsmPrimitive> sel = getLayerManager().getEditDataSet().getSelected();
634
635 // Collecting nodes and ways from the selection
636 for (OsmPrimitive p : sel) {
637 if (p instanceof Way) {
638 wayList.add((Way) p);
639 }
640 if (p instanceof Node) {
641 nodeList.add((Node) p);
642 }
643 }
644
645 if (wayList.size() == 1) {
646 // Starting improving the single selected way
647 startImproving(wayList.get(0));
648 return;
649 } else if (nodeList.size() == 1) {
650 // Starting improving the only way of the single selected node
651 List<OsmPrimitive> r = nodeList.get(0).getReferrers();
652 if (r.size() == 1 && (r.get(0) instanceof Way)) {
653 startImproving((Way) r.get(0));
654 return;
655 }
656 }
657
658 // Starting selecting by default
659 startSelecting();
660 }
661}
Note: See TracBrowser for help on using the repository browser.