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

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

fix #10212, see #10104 - ImproveWayAccuracy: key listeners forgot to remove themselves

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