001// License: GPL. See LICENSE file for details.
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 Way targetWay;
067    private Node candidateNode = null;
068    private WaySegment candidateSegment = null;
069
070    private Point mousePos = null;
071    private boolean dragging = false;
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 Stroke selectTargetWayStroke;
083    private Stroke moveNodeStroke;
084    private Stroke addNodeStroke;
085    private Stroke deleteNodeStroke;
086    private int dotSize;
087
088    private boolean selectionChangedBlocked = false;
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 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}