001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.actions;
003
004import static org.openstreetmap.josm.gui.help.HelpUtil.ht;
005import static org.openstreetmap.josm.tools.I18n.tr;
006
007import java.awt.event.ActionEvent;
008import java.awt.event.KeyEvent;
009import java.util.ArrayList;
010import java.util.Collection;
011import java.util.Collections;
012import java.util.LinkedHashSet;
013import java.util.LinkedList;
014import java.util.List;
015
016import javax.swing.JOptionPane;
017
018import org.openstreetmap.josm.actions.corrector.ReverseWayNoTagCorrector;
019import org.openstreetmap.josm.actions.corrector.ReverseWayTagCorrector;
020import org.openstreetmap.josm.command.ChangeNodesCommand;
021import org.openstreetmap.josm.command.Command;
022import org.openstreetmap.josm.command.SequenceCommand;
023import org.openstreetmap.josm.data.UndoRedoHandler;
024import org.openstreetmap.josm.data.osm.DataSet;
025import org.openstreetmap.josm.data.osm.Node;
026import org.openstreetmap.josm.data.osm.OsmPrimitive;
027import org.openstreetmap.josm.data.osm.Way;
028import org.openstreetmap.josm.gui.Notification;
029import org.openstreetmap.josm.spi.preferences.Config;
030import org.openstreetmap.josm.tools.Logging;
031import org.openstreetmap.josm.tools.Shortcut;
032import org.openstreetmap.josm.tools.UserCancelException;
033
034/**
035 * Reverses the ways that are currently selected by the user
036 */
037public final class ReverseWayAction extends JosmAction {
038
039    /**
040     * The resulting way after reversing it and the commands to get there.
041     */
042    public static class ReverseWayResult {
043        private final Collection<Command> tagCorrectionCommands;
044        private final Command reverseCommand;
045
046        /**
047         * Create a new {@link ReverseWayResult}
048         * @param tagCorrectionCommands The commands to correct the tags
049         * @param reverseCommand The command to reverse the way
050         */
051        public ReverseWayResult(Collection<Command> tagCorrectionCommands, Command reverseCommand) {
052            this.tagCorrectionCommands = tagCorrectionCommands;
053            this.reverseCommand = reverseCommand;
054        }
055
056        /**
057         * Gets the commands that will be required to do a full way reversal including changing the tags
058         * @return The commands
059         */
060        public Collection<Command> getCommands() {
061            List<Command> c = new ArrayList<>();
062            c.addAll(tagCorrectionCommands);
063            c.add(reverseCommand);
064            return c;
065        }
066
067        /**
068         * Gets a single sequence command for reversing this way including changing the tags
069         * @return the command
070         */
071        public Command getAsSequenceCommand() {
072            return new SequenceCommand(tr("Reverse way"), getCommands());
073        }
074
075        /**
076         * Gets the basic reverse command that only changes the order of the nodes.
077         * @return The reorder nodes command
078         */
079        public Command getReverseCommand() {
080            return reverseCommand;
081        }
082
083        /**
084         * Gets the command to change the tags of the way
085         * @return The command to reverse the tags
086         */
087        public Collection<Command> getTagCorrectionCommands() {
088            return tagCorrectionCommands;
089        }
090    }
091
092    /**
093     * Creates a new {@link ReverseWayAction} and binds the shortcut
094     */
095    public ReverseWayAction() {
096        super(tr("Reverse Ways"), "wayflip", tr("Reverse the direction of all selected ways."),
097                Shortcut.registerShortcut("tools:reverse", tr("Tools: {0}", tr("Reverse Ways")), KeyEvent.VK_R, Shortcut.DIRECT), true);
098        setHelpId(ht("/Action/ReverseWays"));
099    }
100
101    @Override
102    public void actionPerformed(ActionEvent e) {
103        DataSet ds = getLayerManager().getEditDataSet();
104        if (!isEnabled() || ds == null)
105            return;
106
107        final Collection<Way> sel = new LinkedHashSet<>(ds.getSelectedWays());
108        sel.removeIf(w -> w.isIncomplete() || w.isEmpty());
109        if (sel.isEmpty()) {
110            new Notification(
111                    tr("Please select at least one way."))
112                    .setIcon(JOptionPane.INFORMATION_MESSAGE)
113                    .setDuration(Notification.TIME_SHORT)
114                    .show();
115            return;
116        }
117
118        Collection<Command> c = new LinkedList<>();
119        for (Way w : sel) {
120            try {
121                c.addAll(reverseWay(w).getCommands());
122            } catch (IllegalArgumentException ex) {
123                Logging.error(ex);
124            } catch (UserCancelException ex) {
125                Logging.trace(ex);
126                return;
127            }
128        }
129        UndoRedoHandler.getInstance().add(new SequenceCommand(tr("Reverse Ways"), c));
130    }
131
132    /**
133     * Reverses a given way.
134     * @param w the way
135     * @return the reverse command and the tag correction commands
136     * @throws IllegalArgumentException if sanity checks fail
137     * @throws UserCancelException if user cancels a reverse warning dialog
138     */
139    public static ReverseWayResult reverseWay(Way w) throws UserCancelException {
140        ReverseWayNoTagCorrector.checkAndConfirmReverseWay(w);
141        List<Node> nodesCopy = w.getNodes();
142        Collections.reverse(nodesCopy);
143
144        Collection<Command> corrCmds = Collections.<Command>emptyList();
145        if (Config.getPref().getBoolean("tag-correction.reverse-way", true)) {
146            corrCmds = new ReverseWayTagCorrector().execute(w, w);
147        }
148        return new ReverseWayResult(corrCmds, new ChangeNodesCommand(w, new ArrayList<>(nodesCopy)));
149    }
150
151    @Override
152    protected void updateEnabledState() {
153        updateEnabledStateOnCurrentSelection();
154    }
155
156    @Override
157    protected void updateEnabledState(Collection<? extends OsmPrimitive> selection) {
158        setEnabled(selection.stream().anyMatch(
159                o -> o instanceof Way && !o.isIncomplete() && !o.getDataSet().isLocked()));
160    }
161}