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