001    // License: GPL. See LICENSE file for details.
002    package org.openstreetmap.josm.actions.mapmode;
003    
004    import static org.openstreetmap.josm.tools.I18n.tr;
005    import static org.openstreetmap.josm.tools.I18n.trn;
006    
007    import java.awt.AWTEvent;
008    import java.awt.BasicStroke;
009    import java.awt.Color;
010    import java.awt.Cursor;
011    import java.awt.Graphics2D;
012    import java.awt.Point;
013    import java.awt.Toolkit;
014    import java.awt.event.AWTEventListener;
015    import java.awt.event.InputEvent;
016    import java.awt.event.KeyEvent;
017    import java.awt.event.MouseEvent;
018    import java.awt.geom.GeneralPath;
019    import java.util.ArrayList;
020    import java.util.Collection;
021    import java.util.Iterator;
022    import java.util.LinkedList;
023    import java.util.List;
024    
025    import javax.swing.JOptionPane;
026    
027    import org.openstreetmap.josm.Main;
028    import org.openstreetmap.josm.command.AddCommand;
029    import org.openstreetmap.josm.command.ChangeCommand;
030    import org.openstreetmap.josm.command.Command;
031    import org.openstreetmap.josm.command.DeleteCommand;
032    import org.openstreetmap.josm.command.MoveCommand;
033    import org.openstreetmap.josm.command.SequenceCommand;
034    import org.openstreetmap.josm.data.Bounds;
035    import org.openstreetmap.josm.data.SelectionChangedListener;
036    import org.openstreetmap.josm.data.coor.EastNorth;
037    import org.openstreetmap.josm.data.osm.DataSet;
038    import org.openstreetmap.josm.data.osm.Node;
039    import org.openstreetmap.josm.data.osm.OsmPrimitive;
040    import org.openstreetmap.josm.data.osm.Way;
041    import org.openstreetmap.josm.data.osm.WaySegment;
042    import org.openstreetmap.josm.data.osm.visitor.paint.PaintColors;
043    import org.openstreetmap.josm.gui.MapFrame;
044    import org.openstreetmap.josm.gui.MapView;
045    import org.openstreetmap.josm.gui.layer.Layer;
046    import org.openstreetmap.josm.gui.layer.MapViewPaintable;
047    import org.openstreetmap.josm.gui.layer.OsmDataLayer;
048    import org.openstreetmap.josm.tools.ImageProvider;
049    import org.openstreetmap.josm.tools.Pair;
050    import org.openstreetmap.josm.tools.Shortcut;
051    
052    /**
053     * @author Alexander Kachkaev <alexander@kachkaev.ru>, 2011
054     */
055    public class ImproveWayAccuracyAction extends MapMode implements MapViewPaintable,
056            SelectionChangedListener, AWTEventListener {
057    
058        enum State {
059            selecting, improving
060        }
061    
062        private State state;
063    
064        private MapView mv;
065    
066        private static final long serialVersionUID = 42L;
067    
068        private Way targetWay;
069        private Node candidateNode = null;
070        private WaySegment candidateSegment = null;
071    
072        private Point mousePos = null;
073        private boolean dragging = false;
074    
075        final private Cursor cursorSelect;
076        final private Cursor cursorSelectHover;
077        final private Cursor cursorImprove;
078        final private Cursor cursorImproveAdd;
079        final private Cursor cursorImproveDelete;
080        final private Cursor cursorImproveAddLock;
081        final private Cursor cursorImproveLock;
082    
083        private final Color guideColor;
084        private final BasicStroke selectTargetWayStroke;
085        private final BasicStroke moveNodeStroke;
086        private final BasicStroke addNodeStroke;
087        private final BasicStroke deleteNodeStroke;
088    
089        private boolean selectionChangedBlocked = false;
090    
091        protected String oldModeHelpText;
092    
093        public ImproveWayAccuracyAction(MapFrame mapFrame) {
094            super(tr("Improve Way Accuracy"), "improvewayaccuracy.png",
095                    tr("Improve Way Accuracy mode"),
096                    Shortcut.registerShortcut("mapmode:ImproveWayAccuracy",
097                    tr("Mode: {0}", tr("Improve Way Accuracy")),
098                    KeyEvent.VK_W, Shortcut.DIRECT), mapFrame, Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR));
099    
100            cursorSelect = ImageProvider.getCursor("normal", "mode");
101            cursorSelectHover = ImageProvider.getCursor("hand", "mode");
102            cursorImprove = ImageProvider.getCursor("crosshair", null);
103            cursorImproveAdd = ImageProvider.getCursor("crosshair", "addnode");
104            cursorImproveDelete = ImageProvider.getCursor("crosshair", "delete_node");
105            cursorImproveAddLock = ImageProvider.getCursor("crosshair",
106                    "add_node_lock");
107            cursorImproveLock = ImageProvider.getCursor("crosshair", "lock");
108    
109            guideColor = PaintColors.HIGHLIGHT.get();
110            selectTargetWayStroke = new BasicStroke(2, BasicStroke.CAP_ROUND,
111                    BasicStroke.JOIN_ROUND);
112            float dash1[] = {4.0f};
113            moveNodeStroke = new BasicStroke(1.0f, BasicStroke.CAP_BUTT,
114                    BasicStroke.JOIN_MITER, 10.0f, dash1, 0.0f);
115            addNodeStroke = new BasicStroke(1, BasicStroke.CAP_BUTT,
116                    BasicStroke.JOIN_MITER);
117            deleteNodeStroke = new BasicStroke(1, BasicStroke.CAP_BUTT,
118                    BasicStroke.JOIN_MITER);
119        }
120    
121        // -------------------------------------------------------------------------
122        // Mode methods
123        // -------------------------------------------------------------------------
124        @Override
125        public void enterMode() {
126            if (!isEnabled()) {
127                return;
128            }
129            super.enterMode();
130    
131            mv = Main.map.mapView;
132            mousePos = null;
133            oldModeHelpText = "";
134    
135            if (getCurrentDataSet() == null) {
136                return;
137            }
138    
139            updateStateByCurrentSelection();
140    
141            Main.map.mapView.addMouseListener(this);
142            Main.map.mapView.addMouseMotionListener(this);
143            Main.map.mapView.addTemporaryLayer(this);
144            DataSet.addSelectionListener(this);
145    
146            try {
147                Toolkit.getDefaultToolkit().addAWTEventListener(this,
148                        AWTEvent.KEY_EVENT_MASK);
149            } catch (SecurityException ex) {
150            }
151        }
152    
153        @Override
154        public void exitMode() {
155            super.exitMode();
156    
157            Main.map.mapView.removeMouseListener(this);
158            Main.map.mapView.removeMouseMotionListener(this);
159            Main.map.mapView.removeTemporaryLayer(this);
160            DataSet.removeSelectionListener(this);
161    
162            try {
163                Toolkit.getDefaultToolkit().removeAWTEventListener(this);
164            } catch (SecurityException ex) {
165            }
166    
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            // setEnabled(Main.main.getActiveLayer() instanceof OsmDataLayer);
208        }
209    
210        // -------------------------------------------------------------------------
211        // MapViewPaintable methods
212        // -------------------------------------------------------------------------
213        /**
214         * Redraws temporary layer. Highlights targetWay in select mode. Draws
215         * preview lines in improve mode and highlights the candidateNode
216         */
217        @Override
218        public void paint(Graphics2D g, MapView mv, Bounds bbox) {
219            if (mousePos == null) {
220                return;
221            }
222    
223            g.setColor(guideColor);
224    
225            if (state == State.selecting && targetWay != null) {
226                // Highlighting the targetWay in Selecting state
227                // Non-native highlighting is used, because sometimes highlighted
228                // segments are covered with others, which is bad.
229                g.setStroke(selectTargetWayStroke);
230    
231                List<Node> nodes = targetWay.getNodes();
232    
233                GeneralPath b = new GeneralPath();
234                Point p0 = mv.getPoint(nodes.get(0));
235                Point pn;
236                b.moveTo(p0.x, p0.y);
237    
238                for (Node n : nodes) {
239                    pn = mv.getPoint(n);
240                    b.lineTo(pn.x, pn.y);
241                }
242                if (targetWay.isClosed()) {
243                    b.lineTo(p0.x, p0.y);
244                }
245    
246                g.draw(b);
247    
248            } else if (state == State.improving) {
249                // Drawing preview lines and highlighting the node
250                // that is going to be moved.
251                // Non-native highlighting is used here as well.
252    
253                // Finding endpoints
254                Point p1 = null, p2 = null;
255                if (ctrl && candidateSegment != null) {
256                    g.setStroke(addNodeStroke);
257                    p1 = mv.getPoint(candidateSegment.getFirstNode());
258                    p2 = mv.getPoint(candidateSegment.getSecondNode());
259                } else if (!alt && !ctrl && candidateNode != null) {
260                    g.setStroke(moveNodeStroke);
261                    List<Pair<Node, Node>> wpps = targetWay.getNodePairs(false);
262                    for (Pair<Node, Node> wpp : wpps) {
263                        if (wpp.a == candidateNode) {
264                            p1 = mv.getPoint(wpp.b);
265                        }
266                        if (wpp.b == candidateNode) {
267                            p2 = mv.getPoint(wpp.a);
268                        }
269                        if (p1 != null && p2 != null) {
270                            break;
271                        }
272                    }
273                } else if (alt && !ctrl && candidateNode != null) {
274                    g.setStroke(deleteNodeStroke);
275                    List<Node> nodes = targetWay.getNodes();
276                    int index = nodes.indexOf(candidateNode);
277    
278                    // Only draw line if node is not first and/or last
279                    if (index != 0 && index != (nodes.size() - 1)) {
280                        p1 = mv.getPoint(nodes.get(index - 1));
281                        p2 = mv.getPoint(nodes.get(index + 1));
282                    }
283                    // TODO: indicate what part that will be deleted? (for end nodes)
284                }
285    
286    
287                // Drawing preview lines
288                GeneralPath b = new GeneralPath();
289                if (alt && !ctrl) {
290                    // In delete mode
291                    if (p1 != null && p2 != null) {
292                        b.moveTo(p1.x, p1.y);
293                        b.lineTo(p2.x, p2.y);
294                    }
295                } else {
296                    // In add or move mode
297                    if (p1 != null) {
298                        b.moveTo(mousePos.x, mousePos.y);
299                        b.lineTo(p1.x, p1.y);
300                    }
301                    if (p2 != null) {
302                        b.moveTo(mousePos.x, mousePos.y);
303                        b.lineTo(p2.x, p2.y);
304                    }
305                }
306                g.draw(b);
307    
308                // Highlighting candidateNode
309                if (candidateNode != null) {
310                    p1 = mv.getPoint(candidateNode);
311                    g.fillRect(p1.x - 2, p1.y - 2, 6, 6);
312                }
313    
314            }
315        }
316    
317        // -------------------------------------------------------------------------
318        // Event handlers
319        // -------------------------------------------------------------------------
320        @Override
321        public void eventDispatched(AWTEvent event) {
322            if (Main.map == null || Main.map.mapView == null
323                    || !Main.map.mapView.isActiveLayerDrawable()) {
324                return;
325            }
326            updateKeyModifiers((InputEvent) event);
327            updateCursorDependentObjectsIfNeeded();
328            updateCursor();
329            updateStatusLine();
330            Main.map.mapView.repaint();
331        }
332    
333        @Override
334        public void selectionChanged(Collection<? extends OsmPrimitive> newSelection) {
335            if (selectionChangedBlocked) {
336                return;
337            }
338            updateStateByCurrentSelection();
339        }
340    
341        @Override
342        public void mouseDragged(MouseEvent e) {
343            dragging = true;
344            mouseMoved(e);
345        }
346    
347        @Override
348        public void mouseMoved(MouseEvent e) {
349            if (!isEnabled()) {
350                return;
351            }
352    
353            mousePos = e.getPoint();
354    
355            updateKeyModifiers(e);
356            updateCursorDependentObjectsIfNeeded();
357            updateCursor();
358            updateStatusLine();
359            Main.map.mapView.repaint();
360        }
361    
362        @Override
363        public void mouseReleased(MouseEvent e) {
364            dragging = false;
365            if (!isEnabled() || e.getButton() != MouseEvent.BUTTON1) {
366                return;
367            }
368    
369            updateKeyModifiers(e);
370            mousePos = e.getPoint();
371    
372            if (state == State.selecting) {
373                if (targetWay != null) {
374                    getCurrentDataSet().setSelected(targetWay.getPrimitiveId());
375                    updateStateByCurrentSelection();
376                }
377            } else if (state == State.improving && mousePos != null) {
378                // Checking if the new coordinate is outside of the world
379                if (mv.getLatLon(mousePos.x, mousePos.y).isOutSideWorld()) {
380                    JOptionPane.showMessageDialog(Main.parent,
381                            tr("Cannot place a node outside of the world."),
382                            tr("Warning"), JOptionPane.WARNING_MESSAGE);
383                    return;
384                }
385    
386                if (ctrl && !alt && candidateSegment != null) {
387                    // Adding a new node to the highlighted segment
388                    // Important: If there are other ways containing the same
389                    // segment, a node must added to all of that ways.
390                    Collection<Command> virtualCmds = new LinkedList<Command>();
391    
392                    // Creating a new node
393                    Node virtualNode = new Node(mv.getEastNorth(mousePos.x,
394                            mousePos.y));
395                    virtualCmds.add(new AddCommand(virtualNode));
396    
397                    // Looking for candidateSegment copies in ways that are
398                    // referenced
399                    // by candidateSegment nodes
400                    List<Way> firstNodeWays = OsmPrimitive.getFilteredList(
401                            candidateSegment.getFirstNode().getReferrers(),
402                            Way.class);
403                    List<Way> secondNodeWays = OsmPrimitive.getFilteredList(
404                            candidateSegment.getFirstNode().getReferrers(),
405                            Way.class);
406    
407                    Collection<WaySegment> virtualSegments = new LinkedList<WaySegment>();
408                    for (Way w : firstNodeWays) {
409                        List<Pair<Node, Node>> wpps = w.getNodePairs(true);
410                        for (Way w2 : secondNodeWays) {
411                            if (!w.equals(w2)) {
412                                continue;
413                            }
414                            // A way is referenced in both nodes.
415                            // Checking if there is such segment
416                            int i = -1;
417                            for (Pair<Node, Node> wpp : wpps) {
418                                ++i;
419                                if ((wpp.a.equals(candidateSegment.getFirstNode())
420                                        && wpp.b.equals(candidateSegment.getSecondNode()) || (wpp.b.equals(candidateSegment.getFirstNode()) && wpp.a.equals(candidateSegment.getSecondNode())))) {
421                                    virtualSegments.add(new WaySegment(w, i));
422                                }
423                            }
424                        }
425                    }
426    
427                    // Adding the node to all segments found
428                    for (WaySegment virtualSegment : virtualSegments) {
429                        Way w = virtualSegment.way;
430                        Way wnew = new Way(w);
431                        wnew.addNode(virtualSegment.lowerIndex + 1, virtualNode);
432                        virtualCmds.add(new ChangeCommand(w, wnew));
433                    }
434    
435                    // Finishing the sequence command
436                    String text = trn("Add and a new node to way",
437                            "Add and a new node to {0} ways",
438                            virtualSegments.size(), virtualSegments.size());
439    
440                    Main.main.undoRedo.add(new SequenceCommand(text, virtualCmds));
441    
442                } else if (alt && !ctrl && candidateNode != null) {
443                    // Deleting the highlighted node
444    
445                    //check to see if node has interesting keys
446                    Iterator<String> keyIterator = candidateNode.getKeys().keySet().iterator();
447                    boolean hasTags = false;
448                    while (keyIterator.hasNext()) {
449                        String key = keyIterator.next();
450                        if (!OsmPrimitive.isUninterestingKey(key)) {
451                            hasTags = true;
452                            break;
453                        }
454                    }
455    
456                    //check to see if node is in use by more than one object
457                    List<OsmPrimitive> referrers = candidateNode.getReferrers();
458                    List<Way> ways = OsmPrimitive.getFilteredList(referrers, Way.class);
459                    if (referrers.size() != 1 || ways.size() != 1) {
460                        JOptionPane.showMessageDialog(Main.parent,
461                                tr("Cannot delete node that is referenced by multiple objects"),
462                                tr("Error"), JOptionPane.ERROR_MESSAGE);
463                    } else if (hasTags) {
464                        JOptionPane.showMessageDialog(Main.parent,
465                                tr("Cannot delete node that has tags"),
466                                tr("Error"), JOptionPane.ERROR_MESSAGE);
467                    } else {
468                        List<Node> nodeList = new ArrayList<Node>();
469                        nodeList.add(candidateNode);
470                        Command deleteCmd = DeleteCommand.delete(getEditLayer(), nodeList, true);
471                        Main.main.undoRedo.add(deleteCmd);
472                    }
473    
474    
475                } else if (candidateNode != null) {
476                    // Moving the highlighted node
477                    EastNorth nodeEN = candidateNode.getEastNorth();
478                    EastNorth cursorEN = mv.getEastNorth(mousePos.x, mousePos.y);
479    
480                    Main.main.undoRedo.add(new MoveCommand(candidateNode, cursorEN.east() - nodeEN.east(), cursorEN.north()
481                            - nodeEN.north()));
482                }
483            }
484    
485            mousePos = null;
486            updateCursor();
487            updateStatusLine();
488            Main.map.mapView.repaint();
489        }
490    
491        @Override
492        public void mouseExited(MouseEvent e) {
493            if (!isEnabled()) {
494                return;
495            }
496    
497            if (!dragging) {
498                mousePos = null;
499            }
500            Main.map.mapView.repaint();
501        }
502    
503        // -------------------------------------------------------------------------
504        // Custom methods
505        // -------------------------------------------------------------------------
506        /**
507         * Sets new cursor depending on state, mouse position
508         */
509        private void updateCursor() {
510            if (!isEnabled()) {
511                mv.setNewCursor(null, this);
512                return;
513            }
514    
515            if (state == State.selecting) {
516                mv.setNewCursor(targetWay == null ? cursorSelect
517                        : cursorSelectHover, this);
518            } else if (state == State.improving) {
519                if (alt && !ctrl) {
520                    mv.setNewCursor(cursorImproveDelete, this);
521                } else if (shift || dragging) {
522                    if (ctrl) {
523                        mv.setNewCursor(cursorImproveAddLock, this);
524                    } else {
525                        mv.setNewCursor(cursorImproveLock, this);
526                    }
527                } else if (ctrl && !alt) {
528                    mv.setNewCursor(cursorImproveAdd, this);
529                } else {
530                    mv.setNewCursor(cursorImprove, this);
531                }
532            }
533        }
534    
535        /**
536         * Updates these objects under cursor: targetWay, candidateNode,
537         * candidateSegment
538         */
539        public void updateCursorDependentObjectsIfNeeded() {
540            if (state == State.improving && (shift || dragging)
541                    && !(candidateNode == null && candidateSegment == null)) {
542                return;
543            }
544    
545            if (mousePos == null) {
546                candidateNode = null;
547                candidateSegment = null;
548                return;
549            }
550    
551            if (state == State.selecting) {
552                targetWay = ImproveWayAccuracyHelper.findWay(mv, mousePos);
553            } else if (state == State.improving) {
554                if (ctrl && !alt) {
555                    candidateSegment = ImproveWayAccuracyHelper.findCandidateSegment(mv,
556                            targetWay, mousePos);
557                    candidateNode = null;
558                } else {
559                    candidateNode = ImproveWayAccuracyHelper.findCandidateNode(mv,
560                            targetWay, mousePos);
561                    candidateSegment = null;
562                }
563            }
564        }
565    
566        /**
567         * Switches to Selecting state
568         */
569        public void startSelecting() {
570            state = State.selecting;
571    
572            targetWay = null;
573            if (getCurrentDataSet() != null) {
574                getCurrentDataSet().clearSelection();
575            }
576    
577            mv.repaint();
578            updateStatusLine();
579        }
580    
581        /**
582         * Switches to Improving state
583         *
584         * @param targetWay Way that is going to be improved
585         */
586        public void startImproving(Way targetWay) {
587            state = State.improving;
588    
589            Collection<OsmPrimitive> currentSelection = getCurrentDataSet().getSelected();
590            if (currentSelection.size() != 1
591                    || !currentSelection.iterator().next().equals(targetWay)) {
592                selectionChangedBlocked = true;
593                getCurrentDataSet().clearSelection();
594                getCurrentDataSet().setSelected(targetWay.getPrimitiveId());
595                selectionChangedBlocked = false;
596            }
597    
598            this.targetWay = targetWay;
599            this.candidateNode = null;
600            this.candidateSegment = null;
601    
602            mv.repaint();
603            updateStatusLine();
604        }
605    
606        /**
607         * Updates the state according to the current selection. Goes to Improve
608         * state if a single way or node is selected. Extracts a way by a node in
609         * the second case.
610         *
611         */
612        private void updateStateByCurrentSelection() {
613            final ArrayList<Node> nodeList = new ArrayList<Node>();
614            final ArrayList<Way> wayList = new ArrayList<Way>();
615            final Collection<OsmPrimitive> sel = getCurrentDataSet().getSelected();
616    
617            // Collecting nodes and ways from the selection
618            for (OsmPrimitive p : sel) {
619                if (p instanceof Way) {
620                    wayList.add((Way) p);
621                }
622                if (p instanceof Node) {
623                    nodeList.add((Node) p);
624                }
625            }
626    
627            if (wayList.size() == 1) {
628                // Starting improving the single selected way
629                startImproving(wayList.get(0));
630                return;
631            } else if (nodeList.size() > 0) {
632                // Starting improving the only way of the single selected node
633                if (nodeList.size() == 1) {
634                    List<OsmPrimitive> r = nodeList.get(0).getReferrers();
635                    if (r.size() == 1 && (r.get(0) instanceof Way)) {
636                        startImproving((Way) r.get(0));
637                        return;
638                    }
639                }
640            }
641    
642            // Starting selecting by default
643            startSelecting();
644        }
645    }