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

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

see #15410 - change preferences scheme for named colors - makes runtime color name registry obsolete

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