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