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

Revision 4982, 23.4 KB checked in by stoecker, 3 months ago (diff)

see #7226 - patch by akks (fixed a bit) - fix shortcut deprecations

Line 
1// License: GPL. See LICENSE file for details.
2package org.openstreetmap.josm.actions.mapmode;
3
4import static org.openstreetmap.josm.tools.I18n.tr;
5import static org.openstreetmap.josm.tools.I18n.trn;
6
7import java.awt.AWTEvent;
8import java.awt.BasicStroke;
9import java.awt.Color;
10import java.awt.Cursor;
11import java.awt.Graphics2D;
12import java.awt.Point;
13import java.awt.Toolkit;
14import java.awt.event.AWTEventListener;
15import java.awt.event.ActionEvent;
16import java.awt.event.InputEvent;
17import java.awt.event.KeyEvent;
18import java.awt.event.MouseEvent;
19import java.awt.geom.GeneralPath;
20import java.util.ArrayList;
21import java.util.Collection;
22import java.util.Iterator;
23import java.util.LinkedList;
24import java.util.List;
25
26import javax.swing.JOptionPane;
27
28import org.openstreetmap.josm.Main;
29import org.openstreetmap.josm.command.AddCommand;
30import org.openstreetmap.josm.command.ChangeCommand;
31import org.openstreetmap.josm.command.Command;
32import org.openstreetmap.josm.command.DeleteCommand;
33import org.openstreetmap.josm.command.MoveCommand;
34import org.openstreetmap.josm.command.SequenceCommand;
35import org.openstreetmap.josm.data.Bounds;
36import org.openstreetmap.josm.data.SelectionChangedListener;
37import org.openstreetmap.josm.data.coor.EastNorth;
38import org.openstreetmap.josm.data.osm.DataSet;
39import org.openstreetmap.josm.data.osm.Node;
40import org.openstreetmap.josm.data.osm.OsmPrimitive;
41import org.openstreetmap.josm.data.osm.Way;
42import org.openstreetmap.josm.data.osm.WaySegment;
43import org.openstreetmap.josm.data.osm.visitor.paint.PaintColors;
44import org.openstreetmap.josm.gui.MapFrame;
45import org.openstreetmap.josm.gui.MapView;
46import org.openstreetmap.josm.gui.layer.Layer;
47import org.openstreetmap.josm.gui.layer.MapViewPaintable;
48import org.openstreetmap.josm.gui.layer.OsmDataLayer;
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 MapViewPaintable,
57        SelectionChangedListener, AWTEventListener {
58
59    enum State {
60        selecting, improving
61    }
62
63    private State state;
64
65    private MapView mv;
66
67    private static final long serialVersionUID = 42L;
68
69    private Way targetWay;
70    private Node candidateNode = null;
71    private WaySegment candidateSegment = null;
72
73    private Point mousePos = null;
74    private boolean dragging = false;
75
76    final private Cursor cursorSelect;
77    final private Cursor cursorSelectHover;
78    final private Cursor cursorImprove;
79    final private Cursor cursorImproveAdd;
80    final private Cursor cursorImproveDelete;
81    final private Cursor cursorImproveAddLock;
82    final private Cursor cursorImproveLock;
83
84    private boolean shift = false;
85    private boolean ctrl = false;
86    private boolean alt = false;
87
88    private final Color guideColor;
89    private final BasicStroke selectTargetWayStroke;
90    private final BasicStroke moveNodeStroke;
91    private final BasicStroke addNodeStroke;
92    private final BasicStroke deleteNodeStroke;
93
94    private boolean selectionChangedBlocked = false;
95
96    protected String oldModeHelpText;
97
98    public ImproveWayAccuracyAction(MapFrame mapFrame) {
99        super(tr("Improve Way Accuracy"), "improvewayaccuracy.png",
100                tr("Improve Way Accuracy mode"),
101                Shortcut.registerShortcut("mapmode:ImproveWayAccuracy",
102                tr("Mode: {0}", tr("Improve Way Accuracy")),
103                KeyEvent.VK_W, Shortcut.DIRECT), mapFrame, Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR));
104
105        cursorSelect = ImageProvider.getCursor("normal", "mode");
106        cursorSelectHover = ImageProvider.getCursor("hand", "mode");
107        cursorImprove = ImageProvider.getCursor("crosshair", null);
108        cursorImproveAdd = ImageProvider.getCursor("crosshair", "addnode");
109        cursorImproveDelete = ImageProvider.getCursor("crosshair", "delete_node");
110        cursorImproveAddLock = ImageProvider.getCursor("crosshair",
111                "add_node_lock");
112        cursorImproveLock = ImageProvider.getCursor("crosshair", "lock");
113
114        guideColor = PaintColors.HIGHLIGHT.get();
115        selectTargetWayStroke = new BasicStroke(2, BasicStroke.CAP_ROUND,
116                BasicStroke.JOIN_ROUND);
117        float dash1[] = {4.0f};
118        moveNodeStroke = new BasicStroke(1.0f, BasicStroke.CAP_BUTT,
119                BasicStroke.JOIN_MITER, 10.0f, dash1, 0.0f);
120        addNodeStroke = new BasicStroke(1, BasicStroke.CAP_BUTT,
121                BasicStroke.JOIN_MITER);
122        deleteNodeStroke = new BasicStroke(1, BasicStroke.CAP_BUTT,
123                BasicStroke.JOIN_MITER);
124    }
125
126    // -------------------------------------------------------------------------
127    // Mode methods
128    // -------------------------------------------------------------------------
129    @Override
130    public void enterMode() {
131        if (!isEnabled()) {
132            return;
133        }
134        super.enterMode();
135
136        mv = Main.map.mapView;
137        mousePos = null;
138        oldModeHelpText = "";
139
140        if (getCurrentDataSet() == 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(this);
149        DataSet.addSelectionListener(this);
150
151        try {
152            Toolkit.getDefaultToolkit().addAWTEventListener(this,
153                    AWTEvent.KEY_EVENT_MASK);
154        } catch (SecurityException ex) {
155        }
156    }
157
158    @Override
159    public void exitMode() {
160        super.exitMode();
161
162        Main.map.mapView.removeMouseListener(this);
163        Main.map.mapView.removeMouseMotionListener(this);
164        Main.map.mapView.removeTemporaryLayer(this);
165        DataSet.removeSelectionListener(this);
166
167        try {
168            Toolkit.getDefaultToolkit().removeAWTEventListener(this);
169        } catch (SecurityException ex) {
170        }
171
172        Main.map.mapView.repaint();
173    }
174
175    @Override
176    protected void updateStatusLine() {
177        String newModeHelpText = getModeHelpText();
178        if (!newModeHelpText.equals(oldModeHelpText)) {
179            oldModeHelpText = newModeHelpText;
180            Main.map.statusLine.setHelpText(newModeHelpText);
181            Main.map.statusLine.repaint();
182        }
183    }
184
185    @Override
186    public String getModeHelpText() {
187        if (state == State.selecting) {
188            if (targetWay != null) {
189                return tr("Click on the way to start improving its shape.");
190            } else {
191                return tr("Select a way that you want to make more accurate.");
192            }
193        } else {
194            if (ctrl) {
195                return tr("Click to add a new node. Release Ctrl to move existing nodes or hold Alt to delete.");
196            } else if (alt) {
197                return tr("Click to delete the highlighted node. Release Alt to move existing nodes or hold Ctrl to add new nodes.");
198            } else {
199                return tr("Click to move the highlighted node. Hold Ctrl to add new nodes, or Alt to delete.");
200            }
201        }
202    }
203
204    @Override
205    public boolean layerIsSupported(Layer l) {
206        return l instanceof OsmDataLayer;
207    }
208
209    @Override
210    protected void updateEnabledState() {
211        setEnabled(getEditLayer() != null);
212        // setEnabled(Main.main.getActiveLayer() instanceof OsmDataLayer);
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     */
222    @Override
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(selectTargetWayStroke);
235
236            List<Node> nodes = targetWay.getNodes();
237
238            GeneralPath b = new GeneralPath();
239            Point p0 = mv.getPoint(nodes.get(0));
240            Point pn;
241            b.moveTo(p0.x, p0.y);
242
243            for (Node n : nodes) {
244                pn = mv.getPoint(n);
245                b.lineTo(pn.x, pn.y);
246            }
247            if (targetWay.isClosed()) {
248                b.lineTo(p0.x, p0.y);
249            }
250
251            g.draw(b);
252
253        } else if (state == State.improving) {
254            // Drawing preview lines and highlighting the node
255            // that is going to be moved.
256            // Non-native highlighting is used here as well.
257
258            // Finding endpoints
259            Point p1 = null, p2 = null;
260            if (ctrl && candidateSegment != null) {
261                g.setStroke(addNodeStroke);
262                p1 = mv.getPoint(candidateSegment.getFirstNode());
263                p2 = mv.getPoint(candidateSegment.getSecondNode());
264            } else if (!alt && !ctrl && candidateNode != null) {
265                g.setStroke(moveNodeStroke);
266                List<Pair<Node, Node>> wpps = targetWay.getNodePairs(false);
267                for (Pair<Node, Node> wpp : wpps) {
268                    if (wpp.a == candidateNode) {
269                        p1 = mv.getPoint(wpp.b);
270                    }
271                    if (wpp.b == candidateNode) {
272                        p2 = mv.getPoint(wpp.a);
273                    }
274                    if (p1 != null && p2 != null) {
275                        break;
276                    }
277                }
278            } else if (alt && !ctrl && candidateNode != null) {
279                g.setStroke(deleteNodeStroke);
280                List<Node> nodes = targetWay.getNodes();
281                int index = nodes.indexOf(candidateNode);
282
283                // Only draw line if node is not first and/or last
284                if (index != 0 && index != (nodes.size() - 1)) {
285                    p1 = mv.getPoint(nodes.get(index - 1));
286                    p2 = mv.getPoint(nodes.get(index + 1));
287                }
288                // TODO: indicate what part that will be deleted? (for end nodes)
289            }
290
291
292            // Drawing preview lines
293            GeneralPath b = new GeneralPath();
294            if (alt && !ctrl) {
295                // In delete mode
296                if (p1 != null && p2 != null) {
297                    b.moveTo(p1.x, p1.y);
298                    b.lineTo(p2.x, p2.y);
299                }
300            } else {
301                // In add or move mode
302                if (p1 != null) {
303                    b.moveTo(mousePos.x, mousePos.y);
304                    b.lineTo(p1.x, p1.y);
305                }
306                if (p2 != null) {
307                    b.moveTo(mousePos.x, mousePos.y);
308                    b.lineTo(p2.x, p2.y);
309                }
310            }
311            g.draw(b);
312
313            // Highlighting candidateNode
314            if (candidateNode != null) {
315                p1 = mv.getPoint(candidateNode);
316                g.fillRect(p1.x - 2, p1.y - 2, 6, 6);
317            }
318
319        }
320    }
321
322    // -------------------------------------------------------------------------
323    // Event handlers
324    // -------------------------------------------------------------------------
325    @Override
326    public void eventDispatched(AWTEvent event) {
327        if (Main.map == null || Main.map.mapView == null
328                || !Main.map.mapView.isActiveLayerDrawable()) {
329            return;
330        }
331        updateKeyModifiers((InputEvent) event);
332        updateCursorDependentObjectsIfNeeded();
333        updateCursor();
334        updateStatusLine();
335        Main.map.mapView.repaint();
336    }
337
338    @Override
339    public void selectionChanged(Collection<? extends OsmPrimitive> newSelection) {
340        if (selectionChangedBlocked) {
341            return;
342        }
343        updateStateByCurrentSelection();
344    }
345
346    @Override
347    public void mouseDragged(MouseEvent e) {
348        dragging = true;
349        mouseMoved(e);
350    }
351
352    @Override
353    public void mouseMoved(MouseEvent e) {
354        if (!isEnabled()) {
355            return;
356        }
357
358        mousePos = e.getPoint();
359
360        updateKeyModifiers(e);
361        updateCursorDependentObjectsIfNeeded();
362        updateCursor();
363        updateStatusLine();
364        Main.map.mapView.repaint();
365    }
366
367    @Override
368    public void mouseReleased(MouseEvent e) {
369        dragging = false;
370        if (!isEnabled() || e.getButton() != MouseEvent.BUTTON1) {
371            return;
372        }
373
374        updateKeyModifiers(e);
375        mousePos = e.getPoint();
376
377        if (state == State.selecting) {
378            if (targetWay != null) {
379                getCurrentDataSet().setSelected(targetWay.getPrimitiveId());
380                updateStateByCurrentSelection();
381            }
382        } else if (state == State.improving && mousePos != null) {
383            // Checking if the new coordinate is outside of the world
384            if (mv.getLatLon(mousePos.x, mousePos.y).isOutSideWorld()) {
385                JOptionPane.showMessageDialog(Main.parent,
386                        tr("Cannot place a node outside of the world."),
387                        tr("Warning"), JOptionPane.WARNING_MESSAGE);
388                return;
389            }
390
391            if (ctrl && !alt && candidateSegment != null) {
392                // Adding a new node to the highlighted segment
393                // Important: If there are other ways containing the same
394                // segment, a node must added to all of that ways.
395                Collection<Command> virtualCmds = new LinkedList<Command>();
396
397                // Creating a new node
398                Node virtualNode = new Node(mv.getEastNorth(mousePos.x,
399                        mousePos.y));
400                virtualCmds.add(new AddCommand(virtualNode));
401
402                // Looking for candidateSegment copies in ways that are
403                // referenced
404                // by candidateSegment nodes
405                List<Way> firstNodeWays = OsmPrimitive.getFilteredList(
406                        candidateSegment.getFirstNode().getReferrers(),
407                        Way.class);
408                List<Way> secondNodeWays = OsmPrimitive.getFilteredList(
409                        candidateSegment.getFirstNode().getReferrers(),
410                        Way.class);
411
412                Collection<WaySegment> virtualSegments = new LinkedList<WaySegment>();
413                for (Way w : firstNodeWays) {
414                    List<Pair<Node, Node>> wpps = w.getNodePairs(true);
415                    for (Way w2 : secondNodeWays) {
416                        if (!w.equals(w2)) {
417                            continue;
418                        }
419                        // A way is referenced in both nodes.
420                        // Checking if there is such segment
421                        int i = -1;
422                        for (Pair<Node, Node> wpp : wpps) {
423                            ++i;
424                            if ((wpp.a.equals(candidateSegment.getFirstNode())
425                                    && wpp.b.equals(candidateSegment.getSecondNode()) || (wpp.b.equals(candidateSegment.getFirstNode()) && wpp.a.equals(candidateSegment.getSecondNode())))) {
426                                virtualSegments.add(new WaySegment(w, i));
427                            }
428                        }
429                    }
430                }
431
432                // Adding the node to all segments found
433                for (WaySegment virtualSegment : virtualSegments) {
434                    Way w = virtualSegment.way;
435                    Way wnew = new Way(w);
436                    wnew.addNode(virtualSegment.lowerIndex + 1, virtualNode);
437                    virtualCmds.add(new ChangeCommand(w, wnew));
438                }
439
440                // Finishing the sequence command
441                String text = trn("Add and a new node to way",
442                        "Add and a new node to {0} ways",
443                        virtualSegments.size(), virtualSegments.size());
444
445                Main.main.undoRedo.add(new SequenceCommand(text, virtualCmds));
446
447            } else if (alt && !ctrl && candidateNode != null) {
448                // Deleting the highlighted node
449
450                //check to see if node has interesting keys
451                Iterator<String> keyIterator = candidateNode.getKeys().keySet().iterator();
452                boolean hasTags = false;
453                while (keyIterator.hasNext()) {
454                    String key = keyIterator.next();
455                    if (!OsmPrimitive.isUninterestingKey(key)) {
456                        hasTags = true;
457                        break;
458                    }
459                }
460
461                //check to see if node is in use by more than one object
462                List<OsmPrimitive> referrers = candidateNode.getReferrers();
463                List<Way> ways = OsmPrimitive.getFilteredList(referrers, Way.class);
464                if (referrers.size() != 1 || ways.size() != 1) {
465                    JOptionPane.showMessageDialog(Main.parent,
466                            tr("Cannot delete node that is referenced by multiple objects"),
467                            tr("Error"), JOptionPane.ERROR_MESSAGE);
468                } else if (hasTags) {
469                    JOptionPane.showMessageDialog(Main.parent,
470                            tr("Cannot delete node that has tags"),
471                            tr("Error"), JOptionPane.ERROR_MESSAGE);
472                } else {
473                    List<Node> nodeList = new ArrayList<Node>();
474                    nodeList.add(candidateNode);
475                    Command deleteCmd = DeleteCommand.delete(getEditLayer(), nodeList, true);
476                    Main.main.undoRedo.add(deleteCmd);
477                }
478
479
480            } else if (candidateNode != null) {
481                // Moving the highlighted node
482                EastNorth nodeEN = candidateNode.getEastNorth();
483                EastNorth cursorEN = mv.getEastNorth(mousePos.x, mousePos.y);
484
485                Main.main.undoRedo.add(new MoveCommand(candidateNode, cursorEN.east() - nodeEN.east(), cursorEN.north()
486                        - nodeEN.north()));
487            }
488        }
489
490        mousePos = null;
491        updateCursor();
492        updateStatusLine();
493        Main.map.mapView.repaint();
494    }
495
496    @Override
497    public void mouseExited(MouseEvent e) {
498        if (!isEnabled()) {
499            return;
500        }
501
502        if (!dragging) {
503            mousePos = null;
504        }
505        Main.map.mapView.repaint();
506    }
507
508    // -------------------------------------------------------------------------
509    // Custom methods
510    // -------------------------------------------------------------------------
511    /**
512     * Updates shift and ctrl key states
513     */
514    @Override
515    protected void updateKeyModifiers(InputEvent e) {
516        ctrl = (e.getModifiers() & ActionEvent.CTRL_MASK) != 0;
517        shift = (e.getModifiers() & ActionEvent.SHIFT_MASK) != 0;
518        // accept either Alt key (including AltGr)
519        alt = ((e.getModifiers() & (ActionEvent.ALT_MASK | InputEvent.ALT_GRAPH_MASK)) != 0);
520    }
521
522    /**
523     * Sets new cursor depending on state, mouse position
524     */
525    private void updateCursor() {
526        if (!isEnabled()) {
527            mv.setNewCursor(null, this);
528            return;
529        }
530
531        if (state == State.selecting) {
532            mv.setNewCursor(targetWay == null ? cursorSelect
533                    : cursorSelectHover, this);
534        } else if (state == State.improving) {
535            if (alt && !ctrl) {
536                mv.setNewCursor(cursorImproveDelete, this);
537            } else if (shift || dragging) {
538                if (ctrl) {
539                    mv.setNewCursor(cursorImproveAddLock, this);
540                } else {
541                    mv.setNewCursor(cursorImproveLock, this);
542                }
543            } else if (ctrl && !alt) {
544                mv.setNewCursor(cursorImproveAdd, this);
545            } else {
546                mv.setNewCursor(cursorImprove, this);
547            }
548        }
549    }
550
551    /**
552     * Updates these objects under cursor: targetWay, candidateNode,
553     * candidateSegment
554     */
555    public void updateCursorDependentObjectsIfNeeded() {
556        if (state == State.improving && (shift || dragging)
557                && !(candidateNode == null && candidateSegment == null)) {
558            return;
559        }
560
561        if (mousePos == null) {
562            candidateNode = null;
563            candidateSegment = null;
564            return;
565        }
566
567        if (state == State.selecting) {
568            targetWay = ImproveWayAccuracyHelper.findWay(mv, mousePos);
569        } else if (state == State.improving) {
570            if (ctrl && !alt) {
571                candidateSegment = ImproveWayAccuracyHelper.findCandidateSegment(mv,
572                        targetWay, mousePos);
573                candidateNode = null;
574            } else {
575                candidateNode = ImproveWayAccuracyHelper.findCandidateNode(mv,
576                        targetWay, mousePos);
577                candidateSegment = null;
578            }
579        }
580    }
581
582    /**
583     * Switches to Selecting state
584     */
585    public void startSelecting() {
586        state = State.selecting;
587
588        targetWay = null;
589        if (getCurrentDataSet() != null) {
590            getCurrentDataSet().clearSelection();
591        }
592
593        mv.repaint();
594        updateStatusLine();
595    }
596
597    /**
598     * Switches to Improving state
599     *
600     * @param targetWay Way that is going to be improved
601     */
602    public void startImproving(Way targetWay) {
603        state = State.improving;
604
605        List<OsmPrimitive> currentSelection = (List<OsmPrimitive>) getCurrentDataSet().getSelected();
606        if (currentSelection.size() != 1
607                || !currentSelection.get(0).equals(targetWay)) {
608            selectionChangedBlocked = true;
609            getCurrentDataSet().clearSelection();
610            getCurrentDataSet().setSelected(targetWay.getPrimitiveId());
611            selectionChangedBlocked = false;
612        }
613
614        this.targetWay = targetWay;
615        this.candidateNode = null;
616        this.candidateSegment = null;
617
618        mv.repaint();
619        updateStatusLine();
620    }
621
622    /**
623     * Updates the state according to the current selection. Goes to Improve
624     * state if a single way or node is selected. Extracts a way by a node in
625     * the second case.
626     *
627     */
628    private void updateStateByCurrentSelection() {
629        final ArrayList<Node> nodeList = new ArrayList<Node>();
630        final ArrayList<Way> wayList = new ArrayList<Way>();
631        final Collection<OsmPrimitive> sel = getCurrentDataSet().getSelected();
632
633        // Collecting nodes and ways from the selection
634        for (OsmPrimitive p : sel) {
635            if (p instanceof Way) {
636                wayList.add((Way) p);
637            }
638            if (p instanceof Node) {
639                nodeList.add((Node) p);
640            }
641        }
642
643        if (wayList.size() == 1) {
644            // Starting improving the single selected way
645            startImproving(wayList.get(0));
646            return;
647        } else if (nodeList.size() > 0) {
648            // Starting improving the only way of the single selected node
649            if (nodeList.size() == 1) {
650                List<OsmPrimitive> r = nodeList.get(0).getReferrers();
651                if (r.size() == 1 && (r.get(0) instanceof Way)) {
652                    startImproving((Way) r.get(0));
653                    return;
654                }
655            }
656        }
657
658        // Starting selecting by default
659        startSelecting();
660    }
661}
Note: See TracBrowser for help on using the repository browser.