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.AWTEvent;
009import java.awt.Color;
010import java.awt.Cursor;
011import java.awt.Graphics2D;
012import java.awt.Point;
013import java.awt.Stroke;
014import java.awt.Toolkit;
015import java.awt.event.AWTEventListener;
016import java.awt.event.InputEvent;
017import java.awt.event.KeyEvent;
018import java.awt.event.MouseEvent;
019import java.awt.geom.GeneralPath;
020import java.util.ArrayList;
021import java.util.Collection;
022import java.util.LinkedList;
023import java.util.List;
024
025import javax.swing.JOptionPane;
026
027import org.openstreetmap.josm.Main;
028import org.openstreetmap.josm.command.AddCommand;
029import org.openstreetmap.josm.command.ChangeCommand;
030import org.openstreetmap.josm.command.Command;
031import org.openstreetmap.josm.command.DeleteCommand;
032import org.openstreetmap.josm.command.MoveCommand;
033import org.openstreetmap.josm.command.SequenceCommand;
034import org.openstreetmap.josm.data.Bounds;
035import org.openstreetmap.josm.data.SelectionChangedListener;
036import org.openstreetmap.josm.data.coor.EastNorth;
037import org.openstreetmap.josm.data.osm.DataSet;
038import org.openstreetmap.josm.data.osm.Node;
039import org.openstreetmap.josm.data.osm.OsmPrimitive;
040import org.openstreetmap.josm.data.osm.Way;
041import org.openstreetmap.josm.data.osm.WaySegment;
042import org.openstreetmap.josm.data.osm.visitor.paint.PaintColors;
043import org.openstreetmap.josm.gui.MapFrame;
044import org.openstreetmap.josm.gui.MapView;
045import org.openstreetmap.josm.gui.layer.Layer;
046import org.openstreetmap.josm.gui.layer.MapViewPaintable;
047import org.openstreetmap.josm.gui.layer.OsmDataLayer;
048import org.openstreetmap.josm.gui.util.GuiHelper;
049import org.openstreetmap.josm.tools.ImageProvider;
050import org.openstreetmap.josm.tools.Pair;
051import org.openstreetmap.josm.tools.Shortcut;
052
053/**
054 * @author Alexander Kachkaev <alexander@kachkaev.ru>, 2011
055 */
056public class ImproveWayAccuracyAction extends MapMode implements MapViewPaintable,
057        SelectionChangedListener, AWTEventListener {
058
059    enum State {
060        selecting, improving
061    }
062
063    private State state;
064
065    private MapView mv;
066
067    private static final long serialVersionUID = 42L;
068
069    private Way targetWay;
070    private Node candidateNode = null;
071    private WaySegment candidateSegment = null;
072
073    private Point mousePos = null;
074    private boolean dragging = false;
075
076    final private Cursor cursorSelect;
077    final private Cursor cursorSelectHover;
078    final private Cursor cursorImprove;
079    final private Cursor cursorImproveAdd;
080    final private Cursor cursorImproveDelete;
081    final private Cursor cursorImproveAddLock;
082    final private Cursor cursorImproveLock;
083
084    private Color guideColor;
085    private Stroke selectTargetWayStroke;
086    private Stroke moveNodeStroke;
087    private Stroke addNodeStroke;
088    private Stroke deleteNodeStroke;
089    private int dotSize;
090
091    private boolean selectionChangedBlocked = false;
092
093    protected String oldModeHelpText;
094
095    public ImproveWayAccuracyAction(MapFrame mapFrame) {
096        super(tr("Improve Way Accuracy"), "improvewayaccuracy.png",
097                tr("Improve Way Accuracy mode"),
098                Shortcut.registerShortcut("mapmode:ImproveWayAccuracy",
099                tr("Mode: {0}", tr("Improve Way Accuracy")),
100                KeyEvent.VK_W, Shortcut.DIRECT), mapFrame, Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR));
101
102        cursorSelect = ImageProvider.getCursor("normal", "mode");
103        cursorSelectHover = ImageProvider.getCursor("hand", "mode");
104        cursorImprove = ImageProvider.getCursor("crosshair", null);
105        cursorImproveAdd = ImageProvider.getCursor("crosshair", "addnode");
106        cursorImproveDelete = ImageProvider.getCursor("crosshair", "delete_node");
107        cursorImproveAddLock = ImageProvider.getCursor("crosshair",
108                "add_node_lock");
109        cursorImproveLock = ImageProvider.getCursor("crosshair", "lock");
110
111    }
112
113    // -------------------------------------------------------------------------
114    // Mode methods
115    // -------------------------------------------------------------------------
116    @Override
117    public void enterMode() {
118        if (!isEnabled()) {
119            return;
120        }
121        super.enterMode();
122
123        guideColor = Main.pref.getColor(marktr("improve way accuracy helper line"), null);
124        if (guideColor == null) guideColor = PaintColors.HIGHLIGHT.get();
125
126        selectTargetWayStroke = GuiHelper.getCustomizedStroke(Main.pref.get("improvewayaccuracy.stroke.select-target", "2"));
127        moveNodeStroke = GuiHelper.getCustomizedStroke(Main.pref.get("improvewayaccuracy.stroke.move-node", "1 6"));
128        addNodeStroke = GuiHelper.getCustomizedStroke(Main.pref.get("improvewayaccuracy.stroke.add-node", "1"));
129        deleteNodeStroke = GuiHelper.getCustomizedStroke(Main.pref.get("improvewayaccuracy.stroke.delete-node", "1"));
130        dotSize = Main.pref.getInteger("improvewayaccuracy.dot-size",6);
131
132        mv = Main.map.mapView;
133        mousePos = null;
134        oldModeHelpText = "";
135
136        if (getCurrentDataSet() == null) {
137            return;
138        }
139
140        updateStateByCurrentSelection();
141
142        Main.map.mapView.addMouseListener(this);
143        Main.map.mapView.addMouseMotionListener(this);
144        Main.map.mapView.addTemporaryLayer(this);
145        DataSet.addSelectionListener(this);
146
147        try {
148            Toolkit.getDefaultToolkit().addAWTEventListener(this,
149                    AWTEvent.KEY_EVENT_MASK);
150        } catch (SecurityException ex) {
151            Main.warn(ex);
152        }
153    }
154
155    @Override
156    public void exitMode() {
157        super.exitMode();
158
159        Main.map.mapView.removeMouseListener(this);
160        Main.map.mapView.removeMouseMotionListener(this);
161        Main.map.mapView.removeTemporaryLayer(this);
162        DataSet.removeSelectionListener(this);
163
164        try {
165            Toolkit.getDefaultToolkit().removeAWTEventListener(this);
166        } catch (SecurityException ex) {
167            Main.warn(ex);
168        }
169
170        Main.map.mapView.repaint();
171    }
172
173    @Override
174    protected void updateStatusLine() {
175        String newModeHelpText = getModeHelpText();
176        if (!newModeHelpText.equals(oldModeHelpText)) {
177            oldModeHelpText = newModeHelpText;
178            Main.map.statusLine.setHelpText(newModeHelpText);
179            Main.map.statusLine.repaint();
180        }
181    }
182
183    @Override
184    public String getModeHelpText() {
185        if (state == State.selecting) {
186            if (targetWay != null) {
187                return tr("Click on the way to start improving its shape.");
188            } else {
189                return tr("Select a way that you want to make more accurate.");
190            }
191        } else {
192            if (ctrl) {
193                return tr("Click to add a new node. Release Ctrl to move existing nodes or hold Alt to delete.");
194            } else if (alt) {
195                return tr("Click to delete the highlighted node. Release Alt to move existing nodes or hold Ctrl to add new nodes.");
196            } else {
197                return tr("Click to move the highlighted node. Hold Ctrl to add new nodes, or Alt to delete.");
198            }
199        }
200    }
201
202    @Override
203    public boolean layerIsSupported(Layer l) {
204        return l instanceof OsmDataLayer;
205    }
206
207    @Override
208    protected void updateEnabledState() {
209        setEnabled(getEditLayer() != null);
210    }
211
212    // -------------------------------------------------------------------------
213    // MapViewPaintable methods
214    // -------------------------------------------------------------------------
215    /**
216     * Redraws temporary layer. Highlights targetWay in select mode. Draws
217     * preview lines in improve mode and highlights the candidateNode
218     */
219    @Override
220    public void paint(Graphics2D g, MapView mv, Bounds bbox) {
221        if (mousePos == null) {
222            return;
223        }
224
225        g.setColor(guideColor);
226
227        if (state == State.selecting && targetWay != null) {
228            // Highlighting the targetWay in Selecting state
229            // Non-native highlighting is used, because sometimes highlighted
230            // segments are covered with others, which is bad.
231            g.setStroke(selectTargetWayStroke);
232
233            List<Node> nodes = targetWay.getNodes();
234
235            GeneralPath b = new GeneralPath();
236            Point p0 = mv.getPoint(nodes.get(0));
237            Point pn;
238            b.moveTo(p0.x, p0.y);
239
240            for (Node n : nodes) {
241                pn = mv.getPoint(n);
242                b.lineTo(pn.x, pn.y);
243            }
244            if (targetWay.isClosed()) {
245                b.lineTo(p0.x, p0.y);
246            }
247
248            g.draw(b);
249
250        } else if (state == State.improving) {
251            // Drawing preview lines and highlighting the node
252            // that is going to be moved.
253            // Non-native highlighting is used here as well.
254
255            // Finding endpoints
256            Point p1 = null, p2 = null;
257            if (ctrl && candidateSegment != null) {
258                g.setStroke(addNodeStroke);
259                p1 = mv.getPoint(candidateSegment.getFirstNode());
260                p2 = mv.getPoint(candidateSegment.getSecondNode());
261            } else if (!alt && !ctrl && candidateNode != null) {
262                g.setStroke(moveNodeStroke);
263                List<Pair<Node, Node>> wpps = targetWay.getNodePairs(false);
264                for (Pair<Node, Node> wpp : wpps) {
265                    if (wpp.a == candidateNode) {
266                        p1 = mv.getPoint(wpp.b);
267                    }
268                    if (wpp.b == candidateNode) {
269                        p2 = mv.getPoint(wpp.a);
270                    }
271                    if (p1 != null && p2 != null) {
272                        break;
273                    }
274                }
275            } else if (alt && !ctrl && candidateNode != null) {
276                g.setStroke(deleteNodeStroke);
277                List<Node> nodes = targetWay.getNodes();
278                int index = nodes.indexOf(candidateNode);
279
280                // Only draw line if node is not first and/or last
281                if (index != 0 && index != (nodes.size() - 1)) {
282                    p1 = mv.getPoint(nodes.get(index - 1));
283                    p2 = mv.getPoint(nodes.get(index + 1));
284                }
285                // TODO: indicate what part that will be deleted? (for end nodes)
286            }
287
288
289            // Drawing preview lines
290            GeneralPath b = new GeneralPath();
291            if (alt && !ctrl) {
292                // In delete mode
293                if (p1 != null && p2 != null) {
294                    b.moveTo(p1.x, p1.y);
295                    b.lineTo(p2.x, p2.y);
296                }
297            } else {
298                // In add or move mode
299                if (p1 != null) {
300                    b.moveTo(mousePos.x, mousePos.y);
301                    b.lineTo(p1.x, p1.y);
302                }
303                if (p2 != null) {
304                    b.moveTo(mousePos.x, mousePos.y);
305                    b.lineTo(p2.x, p2.y);
306                }
307            }
308            g.draw(b);
309
310            // Highlighting candidateNode
311            if (candidateNode != null) {
312                p1 = mv.getPoint(candidateNode);
313                g.fillRect(p1.x - dotSize/2, p1.y - dotSize/2, dotSize, dotSize);
314            }
315
316        }
317    }
318
319    // -------------------------------------------------------------------------
320    // Event handlers
321    // -------------------------------------------------------------------------
322    @Override
323    public void eventDispatched(AWTEvent event) {
324        if (!Main.isDisplayingMapView() || !Main.map.mapView.isActiveLayerDrawable()) {
325            return;
326        }
327        updateKeyModifiers((InputEvent) event);
328        updateCursorDependentObjectsIfNeeded();
329        updateCursor();
330        updateStatusLine();
331        Main.map.mapView.repaint();
332    }
333
334    @Override
335    public void selectionChanged(Collection<? extends OsmPrimitive> newSelection) {
336        if (selectionChangedBlocked) {
337            return;
338        }
339        updateStateByCurrentSelection();
340    }
341
342    @Override
343    public void mouseDragged(MouseEvent e) {
344        dragging = true;
345        mouseMoved(e);
346    }
347
348    @Override
349    public void mouseMoved(MouseEvent e) {
350        if (!isEnabled()) {
351            return;
352        }
353
354        mousePos = e.getPoint();
355
356        updateKeyModifiers(e);
357        updateCursorDependentObjectsIfNeeded();
358        updateCursor();
359        updateStatusLine();
360        Main.map.mapView.repaint();
361    }
362
363    @Override
364    public void mouseReleased(MouseEvent e) {
365        dragging = false;
366        if (!isEnabled() || e.getButton() != MouseEvent.BUTTON1) {
367            return;
368        }
369
370        updateKeyModifiers(e);
371        mousePos = e.getPoint();
372
373        if (state == State.selecting) {
374            if (targetWay != null) {
375                getCurrentDataSet().setSelected(targetWay.getPrimitiveId());
376                updateStateByCurrentSelection();
377            }
378        } else if (state == State.improving && mousePos != null) {
379            // Checking if the new coordinate is outside of the world
380            if (mv.getLatLon(mousePos.x, mousePos.y).isOutSideWorld()) {
381                JOptionPane.showMessageDialog(Main.parent,
382                        tr("Cannot place a node outside of the world."),
383                        tr("Warning"), JOptionPane.WARNING_MESSAGE);
384                return;
385            }
386
387            if (ctrl && !alt && candidateSegment != null) {
388                // Adding a new node to the highlighted segment
389                // Important: If there are other ways containing the same
390                // segment, a node must added to all of that ways.
391                Collection<Command> virtualCmds = new LinkedList<Command>();
392
393                // Creating a new node
394                Node virtualNode = new Node(mv.getEastNorth(mousePos.x,
395                        mousePos.y));
396                virtualCmds.add(new AddCommand(virtualNode));
397
398                // Looking for candidateSegment copies in ways that are
399                // referenced
400                // by candidateSegment nodes
401                List<Way> firstNodeWays = OsmPrimitive.getFilteredList(
402                        candidateSegment.getFirstNode().getReferrers(),
403                        Way.class);
404                List<Way> secondNodeWays = OsmPrimitive.getFilteredList(
405                        candidateSegment.getFirstNode().getReferrers(),
406                        Way.class);
407
408                Collection<WaySegment> virtualSegments = new LinkedList<WaySegment>();
409                for (Way w : firstNodeWays) {
410                    List<Pair<Node, Node>> wpps = w.getNodePairs(true);
411                    for (Way w2 : secondNodeWays) {
412                        if (!w.equals(w2)) {
413                            continue;
414                        }
415                        // A way is referenced in both nodes.
416                        // Checking if there is such segment
417                        int i = -1;
418                        for (Pair<Node, Node> wpp : wpps) {
419                            ++i;
420                            if ((wpp.a.equals(candidateSegment.getFirstNode())
421                                    && wpp.b.equals(candidateSegment.getSecondNode()) || (wpp.b.equals(candidateSegment.getFirstNode()) && wpp.a.equals(candidateSegment.getSecondNode())))) {
422                                virtualSegments.add(new WaySegment(w, i));
423                            }
424                        }
425                    }
426                }
427
428                // Adding the node to all segments found
429                for (WaySegment virtualSegment : virtualSegments) {
430                    Way w = virtualSegment.way;
431                    Way wnew = new Way(w);
432                    wnew.addNode(virtualSegment.lowerIndex + 1, virtualNode);
433                    virtualCmds.add(new ChangeCommand(w, wnew));
434                }
435
436                // Finishing the sequence command
437                String text = trn("Add a new node to way",
438                        "Add a new node to {0} ways",
439                        virtualSegments.size(), virtualSegments.size());
440
441                Main.main.undoRedo.add(new SequenceCommand(text, virtualCmds));
442
443            } else if (alt && !ctrl && candidateNode != null) {
444                // Deleting the highlighted node
445
446                //check to see if node is in use by more than one object
447                List<OsmPrimitive> referrers = candidateNode.getReferrers();
448                List<Way> ways = OsmPrimitive.getFilteredList(referrers, Way.class);
449                if (referrers.size() != 1 || ways.size() != 1) {
450                    JOptionPane.showMessageDialog(Main.parent,
451                            tr("Cannot delete node that is referenced by multiple objects"),
452                            tr("Error"), JOptionPane.ERROR_MESSAGE);
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<Node>();
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<Node>();
603        final List<Way> wayList = new ArrayList<Way>();
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() > 0) {
621            // Starting improving the only way of the single selected node
622            if (nodeList.size() == 1) {
623                List<OsmPrimitive> r = nodeList.get(0).getReferrers();
624                if (r.size() == 1 && (r.get(0) instanceof Way)) {
625                    startImproving((Way) r.get(0));
626                    return;
627                }
628            }
629        }
630
631        // Starting selecting by default
632        startSelecting();
633    }
634}