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