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}