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