001//License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.command;
003
004import java.awt.GridBagLayout;
005import java.util.ArrayList;
006import java.util.Collection;
007import java.util.HashMap;
008import java.util.LinkedHashMap;
009import java.util.Map;
010import java.util.Map.Entry;
011
012import javax.swing.JOptionPane;
013import javax.swing.JPanel;
014
015import org.openstreetmap.josm.Main;
016import org.openstreetmap.josm.data.coor.EastNorth;
017import org.openstreetmap.josm.data.coor.LatLon;
018import org.openstreetmap.josm.data.osm.Node;
019import org.openstreetmap.josm.data.osm.OsmPrimitive;
020import org.openstreetmap.josm.data.osm.PrimitiveData;
021import org.openstreetmap.josm.data.osm.Relation;
022import org.openstreetmap.josm.data.osm.Way;
023import org.openstreetmap.josm.data.osm.visitor.AbstractVisitor;
024import org.openstreetmap.josm.gui.ConditionalOptionPaneUtil;
025import org.openstreetmap.josm.gui.layer.Layer;
026import org.openstreetmap.josm.gui.layer.OsmDataLayer;
027import org.openstreetmap.josm.gui.widgets.JMultilineLabel;
028import org.openstreetmap.josm.tools.CheckParameterUtil;
029
030/**
031 * Classes implementing Command modify a dataset in a specific way. A command is
032 * one atomic action on a specific dataset, such as move or delete.
033 *
034 * The command remembers the {@link OsmDataLayer} it is operating on.
035 *
036 * @author imi
037 */
038public abstract class Command extends PseudoCommand {
039
040    private static final class CloneVisitor extends AbstractVisitor {
041        public final Map<OsmPrimitive, PrimitiveData> orig = new LinkedHashMap<>();
042
043        @Override
044        public void visit(Node n) {
045            orig.put(n, n.save());
046        }
047        @Override
048        public void visit(Way w) {
049            orig.put(w, w.save());
050        }
051        @Override
052        public void visit(Relation e) {
053            orig.put(e, e.save());
054        }
055    }
056
057    /**
058     * Small helper for holding the interesting part of the old data state of the objects.
059     */
060    public static class OldNodeState {
061
062        final LatLon latlon;
063        final EastNorth eastNorth; // cached EastNorth to be used for applying exact displacement
064        final boolean modified;
065
066        /**
067         * Constructs a new {@code OldNodeState} for the given node.
068         * @param node The node whose state has to be remembered
069         */
070        public OldNodeState(Node node){
071            latlon = node.getCoor();
072            eastNorth = node.getEastNorth();
073            modified = node.isModified();
074        }
075    }
076
077    /** the map of OsmPrimitives in the original state to OsmPrimitives in cloned state */
078    private Map<OsmPrimitive, PrimitiveData> cloneMap = new HashMap<>();
079
080    /** the layer which this command is applied to */
081    private final OsmDataLayer layer;
082
083    /**
084     * Creates a new command in the context of the current edit layer, if any
085     */
086    public Command() {
087        this.layer = Main.main == null ? null : Main.main.getEditLayer();
088    }
089
090    /**
091     * Creates a new command in the context of a specific data layer
092     *
093     * @param layer the data layer. Must not be null.
094     * @throws IllegalArgumentException thrown if layer is null
095     */
096    public Command(OsmDataLayer layer) throws IllegalArgumentException {
097        CheckParameterUtil.ensureParameterNotNull(layer, "layer");
098        this.layer = layer;
099    }
100
101    /**
102     * Executes the command on the dataset. This implementation will remember all
103     * primitives returned by fillModifiedData for restoring them on undo.
104     * @return true
105     */
106    public boolean executeCommand() {
107        CloneVisitor visitor = new CloneVisitor();
108        Collection<OsmPrimitive> all = new ArrayList<>();
109        fillModifiedData(all, all, all);
110        for (OsmPrimitive osm : all) {
111            osm.accept(visitor);
112        }
113        cloneMap = visitor.orig;
114        return true;
115    }
116
117    /**
118     * Undoes the command.
119     * It can be assumed that all objects are in the same state they were before.
120     * It can also be assumed that executeCommand was called exactly once before.
121     *
122     * This implementation undoes all objects stored by a former call to executeCommand.
123     */
124    public void undoCommand() {
125        for (Entry<OsmPrimitive, PrimitiveData> e : cloneMap.entrySet()) {
126            OsmPrimitive primitive = e.getKey();
127            if (primitive.getDataSet() != null) {
128                e.getKey().load(e.getValue());
129            }
130        }
131    }
132
133    /**
134     * Called when a layer has been removed to have the command remove itself from
135     * any buffer if it is not longer applicable to the dataset (e.g. it was part of
136     * the removed layer)
137     *
138     * @param oldLayer the old layer
139     * @return true if this command
140     */
141    public boolean invalidBecauselayerRemoved(Layer oldLayer) {
142        if (!(oldLayer instanceof OsmDataLayer))
143            return false;
144        return layer == oldLayer;
145    }
146
147    /**
148     * Lets other commands access the original version
149     * of the object. Usually for undoing.
150     * @param osm The requested OSM object
151     * @return The original version of the requested object, if any
152     */
153    public PrimitiveData getOrig(OsmPrimitive osm) {
154        return cloneMap.get(osm);
155    }
156
157    /**
158     * Replies the layer this command is (or was) applied to.
159     *
160     */
161    protected OsmDataLayer getLayer() {
162        return layer;
163    }
164
165    /**
166     * Fill in the changed data this command operates on.
167     * Add to the lists, don't clear them.
168     *
169     * @param modified The modified primitives
170     * @param deleted The deleted primitives
171     * @param added The added primitives
172     */
173    public abstract void fillModifiedData(Collection<OsmPrimitive> modified,
174            Collection<OsmPrimitive> deleted,
175            Collection<OsmPrimitive> added);
176
177    /**
178     * Return the primitives that take part in this command.
179     */
180    @Override public Collection<? extends OsmPrimitive> getParticipatingPrimitives() {
181        return cloneMap.keySet();
182    }
183
184    /**
185     * Check whether user is about to operate on data outside of the download area.
186     * Request confirmation if he is.
187     *
188     * @param operation the operation name which is used for setting some preferences
189     * @param dialogTitle the title of the dialog being displayed
190     * @param outsideDialogMessage the message text to be displayed when data is outside of the download area
191     * @param incompleteDialogMessage the message text to be displayed when data is incomplete
192     * @param primitives the primitives to operate on
193     * @param ignore {@code null} or a primitive to be ignored
194     * @return true, if operating on outlying primitives is OK; false, otherwise
195     */
196    public static boolean checkAndConfirmOutlyingOperation(String operation,
197            String dialogTitle, String outsideDialogMessage, String incompleteDialogMessage,
198            Collection<? extends OsmPrimitive> primitives,
199            Collection<? extends OsmPrimitive> ignore) {
200        boolean outside = false;
201        boolean incomplete = false;
202        for (OsmPrimitive osm : primitives) {
203            if (osm.isIncomplete()) {
204                incomplete = true;
205            } else if (osm.isOutsideDownloadArea()
206                    && (ignore == null || !ignore.contains(osm))) {
207                outside = true;
208            }
209        }
210        if (outside) {
211            JPanel msg = new JPanel(new GridBagLayout());
212            msg.add(new JMultilineLabel("<html>" + outsideDialogMessage + "</html>"));
213            boolean answer = ConditionalOptionPaneUtil.showConfirmationDialog(
214                    operation + "_outside_nodes",
215                    Main.parent,
216                    msg,
217                    dialogTitle,
218                    JOptionPane.YES_NO_OPTION,
219                    JOptionPane.QUESTION_MESSAGE,
220                    JOptionPane.YES_OPTION);
221            if(!answer)
222                return false;
223        }
224        if (incomplete) {
225            JPanel msg = new JPanel(new GridBagLayout());
226            msg.add(new JMultilineLabel("<html>" + incompleteDialogMessage + "</html>"));
227            boolean answer = ConditionalOptionPaneUtil.showConfirmationDialog(
228                    operation + "_incomplete",
229                    Main.parent,
230                    msg,
231                    dialogTitle,
232                    JOptionPane.YES_NO_OPTION,
233                    JOptionPane.QUESTION_MESSAGE,
234                    JOptionPane.YES_OPTION);
235            if(!answer)
236                return false;
237        }
238        return true;
239    }
240
241}