source: josm/trunk/src/org/openstreetmap/josm/command/Command.java @ 12718

Last change on this file since 12718 was 12718, checked in by Don-vip, 3 weeks ago

see #13036 - see #15229 - see #15182 - make Commands depends only on a DataSet, not a Layer. This removes a lot of GUI dependencies

  • Property svn:eol-style set to native
File size: 13.4 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.command;
3
4import java.awt.GridBagLayout;
5import java.util.ArrayList;
6import java.util.Collection;
7import java.util.HashMap;
8import java.util.LinkedHashMap;
9import java.util.Map;
10import java.util.Map.Entry;
11import java.util.Objects;
12
13import javax.swing.JOptionPane;
14import javax.swing.JPanel;
15
16import org.openstreetmap.josm.Main;
17import org.openstreetmap.josm.data.coor.EastNorth;
18import org.openstreetmap.josm.data.coor.LatLon;
19import org.openstreetmap.josm.data.osm.DataSet;
20import org.openstreetmap.josm.data.osm.Node;
21import org.openstreetmap.josm.data.osm.OsmPrimitive;
22import org.openstreetmap.josm.data.osm.PrimitiveData;
23import org.openstreetmap.josm.data.osm.Relation;
24import org.openstreetmap.josm.data.osm.Way;
25import org.openstreetmap.josm.data.osm.visitor.AbstractVisitor;
26import org.openstreetmap.josm.gui.ConditionalOptionPaneUtil;
27import org.openstreetmap.josm.gui.MainApplication;
28import org.openstreetmap.josm.gui.layer.Layer;
29import org.openstreetmap.josm.gui.layer.OsmDataLayer;
30import org.openstreetmap.josm.gui.widgets.JMultilineLabel;
31import org.openstreetmap.josm.tools.CheckParameterUtil;
32
33/**
34 * Classes implementing Command modify a dataset in a specific way. A command is
35 * one atomic action on a specific dataset, such as move or delete.
36 *
37 * The command remembers the {@link DataSet} it is operating on.
38 *
39 * @author imi
40 * @since 21 (creation)
41 * @since 10599 (signature)
42 */
43public abstract class Command implements PseudoCommand {
44
45    /** IS_OK : operation is okay */
46    public static final int IS_OK = 0;
47    /** IS_OUTSIDE : operation on element outside of download area */
48    public static final int IS_OUTSIDE = 1;
49    /** IS_INCOMPLETE: operation on incomplete target */
50    public static final int IS_INCOMPLETE = 2;
51
52    private static final class CloneVisitor extends AbstractVisitor {
53        public final Map<OsmPrimitive, PrimitiveData> orig = new LinkedHashMap<>();
54
55        @Override
56        public void visit(Node n) {
57            orig.put(n, n.save());
58        }
59
60        @Override
61        public void visit(Way w) {
62            orig.put(w, w.save());
63        }
64
65        @Override
66        public void visit(Relation e) {
67            orig.put(e, e.save());
68        }
69    }
70
71    /**
72     * Small helper for holding the interesting part of the old data state of the objects.
73     */
74    public static class OldNodeState {
75
76        private final LatLon latLon;
77        private final EastNorth eastNorth; // cached EastNorth to be used for applying exact displacement
78        private final boolean modified;
79
80        /**
81         * Constructs a new {@code OldNodeState} for the given node.
82         * @param node The node whose state has to be remembered
83         */
84        public OldNodeState(Node node) {
85            latLon = node.getCoor();
86            eastNorth = node.getEastNorth();
87            modified = node.isModified();
88        }
89
90        /**
91         * Returns old lat/lon.
92         * @return old lat/lon
93         * @see Node#getCoor()
94         * @since 10248
95         */
96        public final LatLon getLatLon() {
97            return latLon;
98        }
99
100        /**
101         * Returns old east/north.
102         * @return old east/north
103         * @see Node#getEastNorth()
104         */
105        public final EastNorth getEastNorth() {
106            return eastNorth;
107        }
108
109        /**
110         * Returns old modified state.
111         * @return old modified state
112         * @see Node #isModified()
113         */
114        public final boolean isModified() {
115            return modified;
116        }
117
118        @Override
119        public int hashCode() {
120            return Objects.hash(latLon, eastNorth, modified);
121        }
122
123        @Override
124        public boolean equals(Object obj) {
125            if (this == obj) return true;
126            if (obj == null || getClass() != obj.getClass()) return false;
127            OldNodeState that = (OldNodeState) obj;
128            return modified == that.modified &&
129                    Objects.equals(latLon, that.latLon) &&
130                    Objects.equals(eastNorth, that.eastNorth);
131        }
132    }
133
134    /** the map of OsmPrimitives in the original state to OsmPrimitives in cloned state */
135    private Map<OsmPrimitive, PrimitiveData> cloneMap = new HashMap<>();
136
137    /**
138     * the layer which this command is applied to
139     * @deprecated to be removed end of 2017. Use {@link #data} instead
140     */
141    @Deprecated
142    private final OsmDataLayer layer;
143
144    /** the dataset which this command is applied to */
145    private final DataSet data;
146
147    /**
148     * Creates a new command in the context of the current edit layer, if any
149     */
150    public Command() {
151        this.layer = MainApplication.getLayerManager().getEditLayer();
152        this.data = layer != null ? layer.data : Main.main.getEditDataSet();
153    }
154
155    /**
156     * Creates a new command in the context of a specific data layer
157     *
158     * @param layer the data layer. Must not be null.
159     * @throws IllegalArgumentException if layer is null
160     * @deprecated to be removed end of 2017. Use {@link #Command(DataSet)} instead
161     */
162    @Deprecated
163    public Command(OsmDataLayer layer) {
164        CheckParameterUtil.ensureParameterNotNull(layer, "layer");
165        this.layer = layer;
166        this.data = layer.data;
167    }
168
169    /**
170     * Creates a new command in the context of a specific data set, without data layer
171     *
172     * @param data the data set. Must not be null.
173     * @throws IllegalArgumentException if data is null
174     * @since 11240
175     */
176    public Command(DataSet data) {
177        CheckParameterUtil.ensureParameterNotNull(data, "data");
178        this.layer = null;
179        this.data = data;
180    }
181
182    /**
183     * Executes the command on the dataset. This implementation will remember all
184     * primitives returned by fillModifiedData for restoring them on undo.
185     * <p>
186     * The layer should be invalidated after execution so that it can be re-painted.
187     * @return true
188     * @see #invalidateAffectedLayers()
189     */
190    public boolean executeCommand() {
191        CloneVisitor visitor = new CloneVisitor();
192        Collection<OsmPrimitive> all = new ArrayList<>();
193        fillModifiedData(all, all, all);
194        for (OsmPrimitive osm : all) {
195            osm.accept(visitor);
196        }
197        cloneMap = visitor.orig;
198        return true;
199    }
200
201    /**
202     * Undoes the command.
203     * It can be assumed that all objects are in the same state they were before.
204     * It can also be assumed that executeCommand was called exactly once before.
205     *
206     * This implementation undoes all objects stored by a former call to executeCommand.
207     */
208    public void undoCommand() {
209        for (Entry<OsmPrimitive, PrimitiveData> e : cloneMap.entrySet()) {
210            OsmPrimitive primitive = e.getKey();
211            if (primitive.getDataSet() != null) {
212                e.getKey().load(e.getValue());
213            }
214        }
215    }
216
217    /**
218     * Called when a layer has been removed to have the command remove itself from
219     * any buffer if it is not longer applicable to the dataset (e.g. it was part of
220     * the removed layer)
221     *
222     * @param oldLayer the old layer that was removed
223     * @return true if this command is invalid after that layer is removed.
224     * @deprecated to be removed end of 2017.
225     */
226    @Deprecated
227    public boolean invalidBecauselayerRemoved(Layer oldLayer) {
228        return layer == oldLayer;
229    }
230
231    /**
232     * Lets other commands access the original version
233     * of the object. Usually for undoing.
234     * @param osm The requested OSM object
235     * @return The original version of the requested object, if any
236     */
237    public PrimitiveData getOrig(OsmPrimitive osm) {
238        return cloneMap.get(osm);
239    }
240
241    /**
242     * Replies the layer this command is (or was) applied to.
243     * @return the layer this command is (or was) applied to
244     * @deprecated to be removed end of 2017. Use {@link #getAffectedDataSet} instead
245     */
246    @Deprecated
247    protected OsmDataLayer getLayer() {
248        return layer;
249    }
250
251    /**
252     * Gets the data set this command affects.
253     * @return The data set. May be <code>null</code> if no layer was set and no edit layer was found.
254     * @since 10467
255     */
256    public DataSet getAffectedDataSet() {
257        return data;
258    }
259
260    /**
261     * Fill in the changed data this command operates on.
262     * Add to the lists, don't clear them.
263     *
264     * @param modified The modified primitives
265     * @param deleted The deleted primitives
266     * @param added The added primitives
267     */
268    public abstract void fillModifiedData(Collection<OsmPrimitive> modified,
269            Collection<OsmPrimitive> deleted,
270            Collection<OsmPrimitive> added);
271
272    /**
273     * Return the primitives that take part in this command.
274     * The collection is computed during execution.
275     */
276    @Override
277    public Collection<? extends OsmPrimitive> getParticipatingPrimitives() {
278        return cloneMap.keySet();
279    }
280
281    /**
282     * Check whether user is about to operate on data outside of the download area.
283     *
284     * @param primitives the primitives to operate on
285     * @param ignore {@code null} or a primitive to be ignored
286     * @return true, if operating on outlying primitives is OK; false, otherwise
287     */
288    public static int checkOutlyingOrIncompleteOperation(
289            Collection<? extends OsmPrimitive> primitives,
290            Collection<? extends OsmPrimitive> ignore) {
291        int res = 0;
292        for (OsmPrimitive osm : primitives) {
293            if (osm.isIncomplete()) {
294                res |= IS_INCOMPLETE;
295            } else if (osm.isOutsideDownloadArea()
296                    && (ignore == null || !ignore.contains(osm))) {
297                res |= IS_OUTSIDE;
298            }
299        }
300        return res;
301    }
302
303    /**
304     * Check whether user is about to operate on data outside of the download area.
305     * Request confirmation if he is.
306     *
307     * @param operation the operation name which is used for setting some preferences
308     * @param dialogTitle the title of the dialog being displayed
309     * @param outsideDialogMessage the message text to be displayed when data is outside of the download area
310     * @param incompleteDialogMessage the message text to be displayed when data is incomplete
311     * @param primitives the primitives to operate on
312     * @param ignore {@code null} or a primitive to be ignored
313     * @return true, if operating on outlying primitives is OK; false, otherwise
314     */
315    public static boolean checkAndConfirmOutlyingOperation(String operation,
316            String dialogTitle, String outsideDialogMessage, String incompleteDialogMessage,
317            Collection<? extends OsmPrimitive> primitives,
318            Collection<? extends OsmPrimitive> ignore) {
319        int checkRes = checkOutlyingOrIncompleteOperation(primitives, ignore);
320        if ((checkRes & IS_OUTSIDE) != 0) {
321            JPanel msg = new JPanel(new GridBagLayout());
322            msg.add(new JMultilineLabel("<html>" + outsideDialogMessage + "</html>"));
323            boolean answer = ConditionalOptionPaneUtil.showConfirmationDialog(
324                    operation + "_outside_nodes",
325                    Main.parent,
326                    msg,
327                    dialogTitle,
328                    JOptionPane.YES_NO_OPTION,
329                    JOptionPane.QUESTION_MESSAGE,
330                    JOptionPane.YES_OPTION);
331            if (!answer)
332                return false;
333        }
334        if ((checkRes & IS_INCOMPLETE) != 0) {
335            JPanel msg = new JPanel(new GridBagLayout());
336            msg.add(new JMultilineLabel("<html>" + incompleteDialogMessage + "</html>"));
337            boolean answer = ConditionalOptionPaneUtil.showConfirmationDialog(
338                    operation + "_incomplete",
339                    Main.parent,
340                    msg,
341                    dialogTitle,
342                    JOptionPane.YES_NO_OPTION,
343                    JOptionPane.QUESTION_MESSAGE,
344                    JOptionPane.YES_OPTION);
345            if (!answer)
346                return false;
347        }
348        return true;
349    }
350
351    /**
352     * Ensures that all primitives that are participating in this command belong to the affected data set.
353     *
354     * Commands may use this in their update methods to check the consitency of the primitives they operate on.
355     * @throws AssertionError if no {@link DataSet} is set or if any primitive does not belong to that dataset.
356     */
357    protected void ensurePrimitivesAreInDataset() {
358        for (OsmPrimitive primitive : this.getParticipatingPrimitives()) {
359            if (primitive.getDataSet() != this.getAffectedDataSet()) {
360                throw new AssertionError("Primitive is of wrong data set for this command: " + primitive);
361            }
362        }
363    }
364
365    @Override
366    public int hashCode() {
367        return Objects.hash(cloneMap, layer, data);
368    }
369
370    @Override
371    public boolean equals(Object obj) {
372        if (this == obj) return true;
373        if (obj == null || getClass() != obj.getClass()) return false;
374        Command command = (Command) obj;
375        return Objects.equals(cloneMap, command.cloneMap) &&
376               Objects.equals(layer, command.layer) &&
377               Objects.equals(data, command.data);
378    }
379
380    /**
381     * Invalidate all layers that were affected by this command.
382     * @see Layer#invalidate()
383     * @deprecated to be removed end of 2017.
384     */
385    @Deprecated
386    public void invalidateAffectedLayers() {
387        OsmDataLayer layer = getLayer();
388        if (layer != null) {
389            layer.invalidate();
390        }
391    }
392}
Note: See TracBrowser for help on using the repository browser.