001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.command;
003
004import java.util.ArrayList;
005import java.util.Collection;
006import java.util.Collections;
007import java.util.LinkedHashMap;
008import java.util.Map;
009import java.util.Map.Entry;
010import java.util.Objects;
011
012import org.openstreetmap.josm.data.coor.EastNorth;
013import org.openstreetmap.josm.data.coor.LatLon;
014import org.openstreetmap.josm.data.osm.DataSet;
015import org.openstreetmap.josm.data.osm.Node;
016import org.openstreetmap.josm.data.osm.OsmPrimitive;
017import org.openstreetmap.josm.data.osm.PrimitiveData;
018import org.openstreetmap.josm.data.osm.Relation;
019import org.openstreetmap.josm.data.osm.Way;
020import org.openstreetmap.josm.data.osm.visitor.OsmPrimitiveVisitor;
021import org.openstreetmap.josm.tools.CheckParameterUtil;
022
023/**
024 * Classes implementing Command modify a dataset in a specific way. A command is
025 * one atomic action on a specific dataset, such as move or delete.
026 *
027 * The command remembers the {@link DataSet} it is operating on.
028 *
029 * @author imi
030 * @since 21 (creation)
031 * @since 10599 (signature)
032 */
033public abstract class Command implements PseudoCommand {
034
035    /** IS_OK : operation is okay */
036    public static final int IS_OK = 0;
037    /** IS_OUTSIDE : operation on element outside of download area */
038    public static final int IS_OUTSIDE = 1;
039    /** IS_INCOMPLETE: operation on incomplete target */
040    public static final int IS_INCOMPLETE = 2;
041
042    private static final class CloneVisitor implements OsmPrimitiveVisitor {
043        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 = Collections.emptyMap();
126
127    /** the dataset which this command is applied to */
128    private final DataSet data;
129
130    /**
131     * Creates a new command in the context of a specific data set, without data layer
132     *
133     * @param data the data set. Must not be null.
134     * @throws IllegalArgumentException if data is null
135     * @since 11240
136     */
137    protected Command(DataSet data) {
138        CheckParameterUtil.ensureParameterNotNull(data, "data");
139        this.data = data;
140    }
141
142    /**
143     * Executes the command on the dataset. This implementation will remember all
144     * primitives returned by fillModifiedData for restoring them on undo.
145     * <p>
146     * The layer should be invalidated after execution so that it can be re-painted.
147     * @return true
148     */
149    public boolean executeCommand() {
150        CloneVisitor visitor = new CloneVisitor();
151        Collection<OsmPrimitive> all = new ArrayList<>();
152        fillModifiedData(all, all, all);
153        for (OsmPrimitive osm : all) {
154            osm.accept(visitor);
155        }
156        cloneMap = visitor.orig;
157        return true;
158    }
159
160    /**
161     * Undoes the command.
162     * It can be assumed that all objects are in the same state they were before.
163     * It can also be assumed that executeCommand was called exactly once before.
164     *
165     * This implementation undoes all objects stored by a former call to executeCommand.
166     */
167    public void undoCommand() {
168        for (Entry<OsmPrimitive, PrimitiveData> e : cloneMap.entrySet()) {
169            OsmPrimitive primitive = e.getKey();
170            if (primitive.getDataSet() != null) {
171                e.getKey().load(e.getValue());
172            }
173        }
174    }
175
176    /**
177     * Lets other commands access the original version
178     * of the object. Usually for undoing.
179     * @param osm The requested OSM object
180     * @return The original version of the requested object, if any
181     */
182    public PrimitiveData getOrig(OsmPrimitive osm) {
183        return cloneMap.get(osm);
184    }
185
186    /**
187     * Gets the data set this command affects.
188     * @return The data set. May be <code>null</code> if no layer was set and no edit layer was found.
189     * @since 10467
190     */
191    public DataSet getAffectedDataSet() {
192        return data;
193    }
194
195    /**
196     * Fill in the changed data this command operates on.
197     * Add to the lists, don't clear them.
198     *
199     * @param modified The modified primitives
200     * @param deleted The deleted primitives
201     * @param added The added primitives
202     */
203    public abstract void fillModifiedData(Collection<OsmPrimitive> modified,
204            Collection<OsmPrimitive> deleted,
205            Collection<OsmPrimitive> added);
206
207    /**
208     * Return the primitives that take part in this command.
209     * The collection is computed during execution.
210     */
211    @Override
212    public Collection<? extends OsmPrimitive> getParticipatingPrimitives() {
213        return cloneMap.keySet();
214    }
215
216    /**
217     * Check whether user is about to operate on data outside of the download area.
218     *
219     * @param primitives the primitives to operate on
220     * @param ignore {@code null} or a primitive to be ignored
221     * @return true, if operating on outlying primitives is OK; false, otherwise
222     */
223    public static int checkOutlyingOrIncompleteOperation(
224            Collection<? extends OsmPrimitive> primitives,
225            Collection<? extends OsmPrimitive> ignore) {
226        int res = 0;
227        for (OsmPrimitive osm : primitives) {
228            if (osm.isIncomplete()) {
229                res |= IS_INCOMPLETE;
230            } else if ((res & IS_OUTSIDE) == 0 && (osm.isOutsideDownloadArea()
231                    || (osm instanceof Node && !osm.isNew() && osm.getDataSet() != null && osm.getDataSet().getDataSourceBounds().isEmpty()))
232                            && (ignore == null || !ignore.contains(osm))) {
233                res |= IS_OUTSIDE;
234            }
235        }
236        return res;
237    }
238
239    /**
240     * Ensures that all primitives that are participating in this command belong to the affected data set.
241     *
242     * Commands may use this in their update methods to check the consistency of the primitives they operate on.
243     * @throws AssertionError if no {@link DataSet} is set or if any primitive does not belong to that dataset.
244     */
245    protected void ensurePrimitivesAreInDataset() {
246        for (OsmPrimitive primitive : this.getParticipatingPrimitives()) {
247            if (primitive.getDataSet() != this.getAffectedDataSet()) {
248                throw new AssertionError("Primitive is of wrong data set for this command: " + primitive);
249            }
250        }
251    }
252
253    @Override
254    public int hashCode() {
255        return Objects.hash(cloneMap, data);
256    }
257
258    @Override
259    public boolean equals(Object obj) {
260        if (this == obj) return true;
261        if (obj == null || getClass() != obj.getClass()) return false;
262        Command command = (Command) obj;
263        return Objects.equals(cloneMap, command.cloneMap) &&
264               Objects.equals(data, command.data);
265    }
266}