001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.actions.mapmode;
003
004import static org.openstreetmap.josm.tools.I18n.marktr;
005import static org.openstreetmap.josm.tools.I18n.tr;
006import static org.openstreetmap.josm.tools.I18n.trn;
007
008import java.awt.Color;
009import java.awt.Cursor;
010import java.awt.Graphics2D;
011import java.awt.Point;
012import java.awt.Stroke;
013import java.awt.event.KeyEvent;
014import java.awt.event.MouseEvent;
015import java.awt.geom.GeneralPath;
016import java.util.ArrayList;
017import java.util.Collection;
018import java.util.LinkedList;
019import java.util.List;
020
021import javax.swing.JOptionPane;
022
023import org.openstreetmap.josm.Main;
024import org.openstreetmap.josm.command.AddCommand;
025import org.openstreetmap.josm.command.ChangeCommand;
026import org.openstreetmap.josm.command.Command;
027import org.openstreetmap.josm.command.DeleteCommand;
028import org.openstreetmap.josm.command.MoveCommand;
029import org.openstreetmap.josm.command.SequenceCommand;
030import org.openstreetmap.josm.data.Bounds;
031import org.openstreetmap.josm.data.SelectionChangedListener;
032import org.openstreetmap.josm.data.coor.EastNorth;
033import org.openstreetmap.josm.data.osm.DataSet;
034import org.openstreetmap.josm.data.osm.Node;
035import org.openstreetmap.josm.data.osm.OsmPrimitive;
036import org.openstreetmap.josm.data.osm.Way;
037import org.openstreetmap.josm.data.osm.WaySegment;
038import org.openstreetmap.josm.data.osm.visitor.paint.PaintColors;
039import org.openstreetmap.josm.gui.MapFrame;
040import org.openstreetmap.josm.gui.MapView;
041import org.openstreetmap.josm.gui.layer.Layer;
042import org.openstreetmap.josm.gui.layer.MapViewPaintable;
043import org.openstreetmap.josm.gui.layer.OsmDataLayer;
044import org.openstreetmap.josm.gui.util.GuiHelper;
045import org.openstreetmap.josm.gui.util.ModifierListener;
046import org.openstreetmap.josm.tools.ImageProvider;
047import org.openstreetmap.josm.tools.Pair;
048import org.openstreetmap.josm.tools.Shortcut;
049
050/**
051 * @author Alexander Kachkaev <alexander@kachkaev.ru>, 2011
052 */
053public class ImproveWayAccuracyAction extends MapMode implements MapViewPaintable,
054        SelectionChangedListener, ModifierListener {
055
056    enum State {
057        selecting, improving
058    }
059
060    private State state;
061
062    private MapView mv;
063
064    private static final long serialVersionUID = 42L;
065
066    private transient Way targetWay;
067    private transient Node candidateNode;
068    private transient WaySegment candidateSegment;
069
070    private Point mousePos;
071    private boolean dragging;
072
073    private final Cursor cursorSelect;
074    private final Cursor cursorSelectHover;
075    private final Cursor cursorImprove;
076    private final Cursor cursorImproveAdd;
077    private final Cursor cursorImproveDelete;
078    private final Cursor cursorImproveAddLock;
079    private final Cursor cursorImproveLock;
080
081    private Color guideColor;
082    private transient Stroke selectTargetWayStroke;
083    private transient Stroke moveNodeStroke;
084    private transient Stroke addNodeStroke;
085    private transient Stroke deleteNodeStroke;
086    private int dotSize;
087
088    private boolean selectionChangedBlocked;
089
090    protected String oldModeHelpText;
091
092    /**
093     * Constructs a new {@code ImproveWayAccuracyAction}.
094     * @param mapFrame Map frame
095     */
096    public ImproveWayAccuracyAction(MapFrame mapFrame) {
097        super(tr("Improve Way Accuracy"), "improvewayaccuracy",
098                tr("Improve Way Accuracy mode"),
099                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 add 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                            boolean ab = wpp.a.equals(candidateSegment.getFirstNode())
415                                    && wpp.b.equals(candidateSegment.getSecondNode());
416                            boolean ba = wpp.b.equals(candidateSegment.getFirstNode())
417                                    && wpp.a.equals(candidateSegment.getSecondNode());
418                            if (ab || ba) {
419                                virtualSegments.add(new WaySegment(w, i));
420                            }
421                        }
422                    }
423                }
424
425                // Adding the node to all segments found
426                for (WaySegment virtualSegment : virtualSegments) {
427                    Way w = virtualSegment.way;
428                    Way wnew = new Way(w);
429                    wnew.addNode(virtualSegment.lowerIndex + 1, virtualNode);
430                    virtualCmds.add(new ChangeCommand(w, wnew));
431                }
432
433                // Finishing the sequence command
434                String text = trn("Add a new node to way",
435                        "Add a new node to {0} ways",
436                        virtualSegments.size(), virtualSegments.size());
437
438                Main.main.undoRedo.add(new SequenceCommand(text, virtualCmds));
439
440            } else if (alt && !ctrl && candidateNode != null) {
441                // Deleting the highlighted node
442
443                //check to see if node is in use by more than one object
444                List<OsmPrimitive> referrers = candidateNode.getReferrers();
445                List<Way> ways = OsmPrimitive.getFilteredList(referrers, Way.class);
446                if (referrers.size() != 1 || ways.size() != 1) {
447                    // detach node from way
448                    final Way newWay = new Way(targetWay);
449                    final List<Node> nodes = newWay.getNodes();
450                    nodes.remove(candidateNode);
451                    newWay.setNodes(nodes);
452                    Main.main.undoRedo.add(new ChangeCommand(targetWay, newWay));
453                } else if (candidateNode.isTagged()) {
454                    JOptionPane.showMessageDialog(Main.parent,
455                            tr("Cannot delete node that has tags"),
456                            tr("Error"), JOptionPane.ERROR_MESSAGE);
457                } else {
458                    List<Node> nodeList = new ArrayList<>();
459                    nodeList.add(candidateNode);
460                    Command deleteCmd = DeleteCommand.delete(getEditLayer(), nodeList, true);
461                    if (deleteCmd != null) {
462                        Main.main.undoRedo.add(deleteCmd);
463                    }
464                }
465
466
467            } else if (candidateNode != null) {
468                // Moving the highlighted node
469                EastNorth nodeEN = candidateNode.getEastNorth();
470                EastNorth cursorEN = mv.getEastNorth(mousePos.x, mousePos.y);
471
472                Main.main.undoRedo.add(new MoveCommand(candidateNode, cursorEN.east() - nodeEN.east(), cursorEN.north()
473                        - nodeEN.north()));
474            }
475        }
476
477        mousePos = null;
478        updateCursor();
479        updateStatusLine();
480        Main.map.mapView.repaint();
481    }
482
483    @Override
484    public void mouseExited(MouseEvent e) {
485        if (!isEnabled()) {
486            return;
487        }
488
489        if (!dragging) {
490            mousePos = null;
491        }
492        Main.map.mapView.repaint();
493    }
494
495    // -------------------------------------------------------------------------
496    // Custom methods
497    // -------------------------------------------------------------------------
498    /**
499     * Sets new cursor depending on state, mouse position
500     */
501    private void updateCursor() {
502        if (!isEnabled()) {
503            mv.setNewCursor(null, this);
504            return;
505        }
506
507        if (state == State.selecting) {
508            mv.setNewCursor(targetWay == null ? cursorSelect
509                    : cursorSelectHover, this);
510        } else if (state == State.improving) {
511            if (alt && !ctrl) {
512                mv.setNewCursor(cursorImproveDelete, this);
513            } else if (shift || dragging) {
514                if (ctrl) {
515                    mv.setNewCursor(cursorImproveAddLock, this);
516                } else {
517                    mv.setNewCursor(cursorImproveLock, this);
518                }
519            } else if (ctrl && !alt) {
520                mv.setNewCursor(cursorImproveAdd, this);
521            } else {
522                mv.setNewCursor(cursorImprove, this);
523            }
524        }
525    }
526
527    /**
528     * Updates these objects under cursor: targetWay, candidateNode,
529     * candidateSegment
530     */
531    public void updateCursorDependentObjectsIfNeeded() {
532        if (state == State.improving && (shift || dragging)
533                && !(candidateNode == null && candidateSegment == null)) {
534            return;
535        }
536
537        if (mousePos == null) {
538            candidateNode = null;
539            candidateSegment = null;
540            return;
541        }
542
543        if (state == State.selecting) {
544            targetWay = ImproveWayAccuracyHelper.findWay(mv, mousePos);
545        } else if (state == State.improving) {
546            if (ctrl && !alt) {
547                candidateSegment = ImproveWayAccuracyHelper.findCandidateSegment(mv,
548                        targetWay, mousePos);
549                candidateNode = null;
550            } else {
551                candidateNode = ImproveWayAccuracyHelper.findCandidateNode(mv,
552                        targetWay, mousePos);
553                candidateSegment = null;
554            }
555        }
556    }
557
558    /**
559     * Switches to Selecting state
560     */
561    public void startSelecting() {
562        state = State.selecting;
563
564        targetWay = null;
565
566        mv.repaint();
567        updateStatusLine();
568    }
569
570    /**
571     * Switches to Improving state
572     *
573     * @param targetWay Way that is going to be improved
574     */
575    public void startImproving(Way targetWay) {
576        state = State.improving;
577
578        Collection<OsmPrimitive> currentSelection = getCurrentDataSet().getSelected();
579        if (currentSelection.size() != 1
580                || !currentSelection.iterator().next().equals(targetWay)) {
581            selectionChangedBlocked = true;
582            getCurrentDataSet().clearSelection();
583            getCurrentDataSet().setSelected(targetWay.getPrimitiveId());
584            selectionChangedBlocked = false;
585        }
586
587        this.targetWay = targetWay;
588        this.candidateNode = null;
589        this.candidateSegment = null;
590
591        mv.repaint();
592        updateStatusLine();
593    }
594
595    /**
596     * Updates the state according to the current selection. Goes to Improve
597     * state if a single way or node is selected. Extracts a way by a node in
598     * the second case.
599     *
600     */
601    private void updateStateByCurrentSelection() {
602        final List<Node> nodeList = new ArrayList<>();
603        final List<Way> wayList = new ArrayList<>();
604        final Collection<OsmPrimitive> sel = getCurrentDataSet().getSelected();
605
606        // Collecting nodes and ways from the selection
607        for (OsmPrimitive p : sel) {
608            if (p instanceof Way) {
609                wayList.add((Way) p);
610            }
611            if (p instanceof Node) {
612                nodeList.add((Node) p);
613            }
614        }
615
616        if (wayList.size() == 1) {
617            // Starting improving the single selected way
618            startImproving(wayList.get(0));
619            return;
620        } else if (nodeList.size() == 1) {
621            // Starting improving the only way of the single selected node
622            List<OsmPrimitive> r = nodeList.get(0).getReferrers();
623            if (r.size() == 1 && (r.get(0) instanceof Way)) {
624                startImproving((Way) r.get(0));
625                return;
626            }
627        }
628
629        // Starting selecting by default
630        startSelecting();
631    }
632}