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