[8378] | 1 | // License: GPL. For details, see LICENSE file.
|
---|
[21] | 2 | package org.openstreetmap.josm.command;
|
---|
| 3 |
|
---|
[3034] | 4 | import java.util.ArrayList;
|
---|
[22] | 5 | import java.util.Collection;
|
---|
[146] | 6 | import java.util.HashMap;
|
---|
[3034] | 7 | import java.util.LinkedHashMap;
|
---|
[146] | 8 | import java.util.Map;
|
---|
[86] | 9 | import java.util.Map.Entry;
|
---|
[9371] | 10 | import java.util.Objects;
|
---|
[21] | 11 |
|
---|
[6173] | 12 | import org.openstreetmap.josm.data.coor.EastNorth;
|
---|
| 13 | import org.openstreetmap.josm.data.coor.LatLon;
|
---|
[10467] | 14 | import org.openstreetmap.josm.data.osm.DataSet;
|
---|
[146] | 15 | import org.openstreetmap.josm.data.osm.Node;
|
---|
[22] | 16 | import org.openstreetmap.josm.data.osm.OsmPrimitive;
|
---|
[2284] | 17 | import org.openstreetmap.josm.data.osm.PrimitiveData;
|
---|
[1523] | 18 | import org.openstreetmap.josm.data.osm.Relation;
|
---|
[146] | 19 | import org.openstreetmap.josm.data.osm.Way;
|
---|
[12809] | 20 | import org.openstreetmap.josm.data.osm.visitor.OsmPrimitiveVisitor;
|
---|
[2844] | 21 | import org.openstreetmap.josm.tools.CheckParameterUtil;
|
---|
[21] | 22 |
|
---|
| 23 | /**
|
---|
| 24 | * Classes implementing Command modify a dataset in a specific way. A command is
|
---|
[22] | 25 | * one atomic action on a specific dataset, such as move or delete.
|
---|
[21] | 26 | *
|
---|
[12718] | 27 | * The command remembers the {@link DataSet} it is operating on.
|
---|
[2284] | 28 | *
|
---|
[21] | 29 | * @author imi
|
---|
[10599] | 30 | * @since 21 (creation)
|
---|
| 31 | * @since 10599 (signature)
|
---|
[21] | 32 | */
|
---|
[10599] | 33 | public abstract class Command implements PseudoCommand {
|
---|
[21] | 34 |
|
---|
[10948] | 35 | /** IS_OK : operation is okay */
|
---|
| 36 | public static final int IS_OK = 0;
|
---|
| 37 | /** IS_OUTSIDE : operation on element outside of download area */
|
---|
| 38 | public static final int IS_OUTSIDE = 1;
|
---|
| 39 | /** IS_INCOMPLETE: operation on incomplete target */
|
---|
| 40 | public static final int IS_INCOMPLETE = 2;
|
---|
| 41 |
|
---|
[12809] | 42 | private static final class CloneVisitor implements OsmPrimitiveVisitor {
|
---|
[13158] | 43 | final Map<OsmPrimitive, PrimitiveData> orig = new LinkedHashMap<>();
|
---|
[304] | 44 |
|
---|
[6084] | 45 | @Override
|
---|
[1750] | 46 | public void visit(Node n) {
|
---|
[2284] | 47 | orig.put(n, n.save());
|
---|
[1750] | 48 | }
|
---|
[8510] | 49 |
|
---|
[6084] | 50 | @Override
|
---|
[1750] | 51 | public void visit(Way w) {
|
---|
[2284] | 52 | orig.put(w, w.save());
|
---|
[1750] | 53 | }
|
---|
[8510] | 54 |
|
---|
[6084] | 55 | @Override
|
---|
[1750] | 56 | public void visit(Relation e) {
|
---|
[2284] | 57 | orig.put(e, e.save());
|
---|
[1750] | 58 | }
|
---|
| 59 | }
|
---|
[304] | 60 |
|
---|
[6173] | 61 | /**
|
---|
| 62 | * Small helper for holding the interesting part of the old data state of the objects.
|
---|
| 63 | */
|
---|
| 64 | public static class OldNodeState {
|
---|
| 65 |
|
---|
[10248] | 66 | private final LatLon latLon;
|
---|
[8285] | 67 | private final EastNorth eastNorth; // cached EastNorth to be used for applying exact displacement
|
---|
| 68 | private final boolean modified;
|
---|
[6173] | 69 |
|
---|
| 70 | /**
|
---|
| 71 | * Constructs a new {@code OldNodeState} for the given node.
|
---|
| 72 | * @param node The node whose state has to be remembered
|
---|
| 73 | */
|
---|
[8510] | 74 | public OldNodeState(Node node) {
|
---|
[10248] | 75 | latLon = node.getCoor();
|
---|
[6173] | 76 | eastNorth = node.getEastNorth();
|
---|
| 77 | modified = node.isModified();
|
---|
| 78 | }
|
---|
[8285] | 79 |
|
---|
| 80 | /**
|
---|
| 81 | * Returns old lat/lon.
|
---|
| 82 | * @return old lat/lon
|
---|
| 83 | * @see Node#getCoor()
|
---|
[10248] | 84 | * @since 10248
|
---|
[8285] | 85 | */
|
---|
[10248] | 86 | public final LatLon getLatLon() {
|
---|
| 87 | return latLon;
|
---|
[8285] | 88 | }
|
---|
| 89 |
|
---|
| 90 | /**
|
---|
| 91 | * Returns old east/north.
|
---|
| 92 | * @return old east/north
|
---|
| 93 | * @see Node#getEastNorth()
|
---|
| 94 | */
|
---|
| 95 | public final EastNorth getEastNorth() {
|
---|
| 96 | return eastNorth;
|
---|
| 97 | }
|
---|
| 98 |
|
---|
| 99 | /**
|
---|
| 100 | * Returns old modified state.
|
---|
| 101 | * @return old modified state
|
---|
| 102 | * @see Node #isModified()
|
---|
| 103 | */
|
---|
| 104 | public final boolean isModified() {
|
---|
| 105 | return modified;
|
---|
| 106 | }
|
---|
[8447] | 107 |
|
---|
| 108 | @Override
|
---|
| 109 | public int hashCode() {
|
---|
[10248] | 110 | return Objects.hash(latLon, eastNorth, modified);
|
---|
[8447] | 111 | }
|
---|
| 112 |
|
---|
| 113 | @Override
|
---|
| 114 | public boolean equals(Object obj) {
|
---|
[9371] | 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 &&
|
---|
[10248] | 119 | Objects.equals(latLon, that.latLon) &&
|
---|
[9371] | 120 | Objects.equals(eastNorth, that.eastNorth);
|
---|
[8447] | 121 | }
|
---|
[6173] | 122 | }
|
---|
| 123 |
|
---|
[1750] | 124 | /** the map of OsmPrimitives in the original state to OsmPrimitives in cloned state */
|
---|
[7005] | 125 | private Map<OsmPrimitive, PrimitiveData> cloneMap = new HashMap<>();
|
---|
[304] | 126 |
|
---|
[11240] | 127 | /** the dataset which this command is applied to */
|
---|
| 128 | private final DataSet data;
|
---|
| 129 |
|
---|
[5759] | 130 | /**
|
---|
[11240] | 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 | public Command(DataSet data) {
|
---|
| 138 | CheckParameterUtil.ensureParameterNotNull(data, "data");
|
---|
| 139 | this.data = data;
|
---|
| 140 | }
|
---|
| 141 |
|
---|
| 142 | /**
|
---|
[1750] | 143 | * Executes the command on the dataset. This implementation will remember all
|
---|
| 144 | * primitives returned by fillModifiedData for restoring them on undo.
|
---|
[10452] | 145 | * <p>
|
---|
| 146 | * The layer should be invalidated after execution so that it can be re-painted.
|
---|
[5759] | 147 | * @return true
|
---|
[1750] | 148 | */
|
---|
| 149 | public boolean executeCommand() {
|
---|
| 150 | CloneVisitor visitor = new CloneVisitor();
|
---|
[7005] | 151 | Collection<OsmPrimitive> all = new ArrayList<>();
|
---|
[1750] | 152 | fillModifiedData(all, all, all);
|
---|
| 153 | for (OsmPrimitive osm : all) {
|
---|
[6009] | 154 | osm.accept(visitor);
|
---|
[1750] | 155 | }
|
---|
| 156 | cloneMap = visitor.orig;
|
---|
| 157 | return true;
|
---|
| 158 | }
|
---|
[86] | 159 |
|
---|
[1750] | 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() {
|
---|
[2284] | 168 | for (Entry<OsmPrimitive, PrimitiveData> e : cloneMap.entrySet()) {
|
---|
[2683] | 169 | OsmPrimitive primitive = e.getKey();
|
---|
| 170 | if (primitive.getDataSet() != null) {
|
---|
| 171 | e.getKey().load(e.getValue());
|
---|
| 172 | }
|
---|
[1750] | 173 | }
|
---|
| 174 | }
|
---|
[304] | 175 |
|
---|
[1750] | 176 | /**
|
---|
[630] | 177 | * Lets other commands access the original version
|
---|
| 178 | * of the object. Usually for undoing.
|
---|
[5759] | 179 | * @param osm The requested OSM object
|
---|
| 180 | * @return The original version of the requested object, if any
|
---|
[630] | 181 | */
|
---|
[2284] | 182 | public PrimitiveData getOrig(OsmPrimitive osm) {
|
---|
[5759] | 183 | return cloneMap.get(osm);
|
---|
[630] | 184 | }
|
---|
[94] | 185 |
|
---|
[1750] | 186 | /**
|
---|
[10467] | 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() {
|
---|
[11240] | 192 | return data;
|
---|
[10467] | 193 | }
|
---|
| 194 |
|
---|
| 195 | /**
|
---|
[1750] | 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 | */
|
---|
[6883] | 203 | public abstract void fillModifiedData(Collection<OsmPrimitive> modified,
|
---|
[1750] | 204 | Collection<OsmPrimitive> deleted,
|
---|
| 205 | Collection<OsmPrimitive> added);
|
---|
| 206 |
|
---|
[3262] | 207 | /**
|
---|
| 208 | * Return the primitives that take part in this command.
|
---|
[8945] | 209 | * The collection is computed during execution.
|
---|
[3262] | 210 | */
|
---|
[8931] | 211 | @Override
|
---|
| 212 | public Collection<? extends OsmPrimitive> getParticipatingPrimitives() {
|
---|
[3262] | 213 | return cloneMap.keySet();
|
---|
| 214 | }
|
---|
[1750] | 215 |
|
---|
[3262] | 216 | /**
|
---|
[4458] | 217 | * Check whether user is about to operate on data outside of the download area.
|
---|
[10948] | 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 | */
|
---|
[10970] | 223 | public static int checkOutlyingOrIncompleteOperation(
|
---|
[10948] | 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 (osm.isOutsideDownloadArea()
|
---|
| 231 | && (ignore == null || !ignore.contains(osm))) {
|
---|
| 232 | res |= IS_OUTSIDE;
|
---|
| 233 | }
|
---|
| 234 | }
|
---|
| 235 | return res;
|
---|
| 236 | }
|
---|
| 237 |
|
---|
| 238 | /**
|
---|
[12348] | 239 | * Ensures that all primitives that are participating in this command belong to the affected data set.
|
---|
| 240 | *
|
---|
| 241 | * Commands may use this in their update methods to check the consitency of the primitives they operate on.
|
---|
| 242 | * @throws AssertionError if no {@link DataSet} is set or if any primitive does not belong to that dataset.
|
---|
| 243 | */
|
---|
| 244 | protected void ensurePrimitivesAreInDataset() {
|
---|
| 245 | for (OsmPrimitive primitive : this.getParticipatingPrimitives()) {
|
---|
| 246 | if (primitive.getDataSet() != this.getAffectedDataSet()) {
|
---|
| 247 | throw new AssertionError("Primitive is of wrong data set for this command: " + primitive);
|
---|
| 248 | }
|
---|
| 249 | }
|
---|
| 250 | }
|
---|
| 251 |
|
---|
[8447] | 252 | @Override
|
---|
| 253 | public int hashCode() {
|
---|
[13173] | 254 | return Objects.hash(cloneMap, data);
|
---|
[8447] | 255 | }
|
---|
| 256 |
|
---|
| 257 | @Override
|
---|
| 258 | public boolean equals(Object obj) {
|
---|
[9371] | 259 | if (this == obj) return true;
|
---|
| 260 | if (obj == null || getClass() != obj.getClass()) return false;
|
---|
| 261 | Command command = (Command) obj;
|
---|
| 262 | return Objects.equals(cloneMap, command.cloneMap) &&
|
---|
[11240] | 263 | Objects.equals(data, command.data);
|
---|
[8447] | 264 | }
|
---|
[21] | 265 | }
|
---|