001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.command; 003 004import static org.openstreetmap.josm.tools.I18n.trn; 005 006import java.util.Collection; 007import java.util.Collections; 008import java.util.Iterator; 009import java.util.LinkedList; 010import java.util.List; 011import java.util.NoSuchElementException; 012import java.util.Objects; 013 014import javax.swing.Icon; 015 016import org.openstreetmap.josm.data.coor.EastNorth; 017import org.openstreetmap.josm.data.coor.LatLon; 018import org.openstreetmap.josm.data.osm.DataSet; 019import org.openstreetmap.josm.data.osm.Node; 020import org.openstreetmap.josm.data.osm.OsmPrimitive; 021import org.openstreetmap.josm.data.osm.visitor.AllNodesVisitor; 022import org.openstreetmap.josm.data.projection.ProjectionRegistry; 023import org.openstreetmap.josm.tools.ImageProvider; 024 025/** 026 * MoveCommand moves a set of OsmPrimitives along the map. It can be moved again 027 * to collect several MoveCommands into one command. 028 * 029 * @author imi 030 */ 031public class MoveCommand extends Command { 032 /** 033 * The objects that should be moved. 034 */ 035 private Collection<Node> nodes = new LinkedList<>(); 036 /** 037 * Starting position, base command point, current (mouse-drag) position = startEN + (x,y) = 038 */ 039 private EastNorth startEN; 040 041 /** 042 * x difference movement. Coordinates are in northern/eastern 043 */ 044 private double x; 045 /** 046 * y difference movement. Coordinates are in northern/eastern 047 */ 048 private double y; 049 050 private double backupX; 051 private double backupY; 052 053 /** 054 * List of all old states of the objects. 055 */ 056 private final List<OldNodeState> oldState = new LinkedList<>(); 057 058 /** 059 * Constructs a new {@code MoveCommand} to move a primitive. 060 * @param osm The primitive to move 061 * @param x X difference movement. Coordinates are in northern/eastern 062 * @param y Y difference movement. Coordinates are in northern/eastern 063 */ 064 public MoveCommand(OsmPrimitive osm, double x, double y) { 065 this(Collections.singleton(osm), x, y); 066 } 067 068 /** 069 * Constructs a new {@code MoveCommand} to move a node. 070 * @param node The node to move 071 * @param position The new location (lat/lon) 072 */ 073 public MoveCommand(Node node, LatLon position) { 074 this(Collections.singleton((OsmPrimitive) node), 075 ProjectionRegistry.getProjection().latlon2eastNorth(position).subtract(node.getEastNorth())); 076 } 077 078 /** 079 * Constructs a new {@code MoveCommand} to move a collection of primitives. 080 * @param objects The primitives to move 081 * @param offset The movement vector 082 */ 083 public MoveCommand(Collection<OsmPrimitive> objects, EastNorth offset) { 084 this(objects, offset.getX(), offset.getY()); 085 } 086 087 /** 088 * Constructs a new {@code MoveCommand} and assign the initial object set and movement vector. 089 * @param objects The primitives to move. Must neither be null nor empty. Objects must belong to a data set 090 * @param x X difference movement. Coordinates are in northern/eastern 091 * @param y Y difference movement. Coordinates are in northern/eastern 092 * @throws NullPointerException if objects is null or contain null item 093 * @throws NoSuchElementException if objects is empty 094 */ 095 public MoveCommand(Collection<OsmPrimitive> objects, double x, double y) { 096 this(objects.iterator().next().getDataSet(), objects, x, y); 097 } 098 099 /** 100 * Constructs a new {@code MoveCommand} and assign the initial object set and movement vector. 101 * @param ds the dataset context for moving these primitives. Must not be null. 102 * @param objects The primitives to move. Must neither be null. 103 * @param x X difference movement. Coordinates are in northern/eastern 104 * @param y Y difference movement. Coordinates are in northern/eastern 105 * @throws NullPointerException if objects is null or contain null item 106 * @throws NoSuchElementException if objects is empty 107 * @since 12759 108 */ 109 public MoveCommand(DataSet ds, Collection<OsmPrimitive> objects, double x, double y) { 110 super(ds); 111 startEN = null; 112 saveCheckpoint(); // (0,0) displacement will be saved 113 this.x = x; 114 this.y = y; 115 Objects.requireNonNull(objects, "objects"); 116 this.nodes = AllNodesVisitor.getAllNodes(objects); 117 for (Node n : this.nodes) { 118 oldState.add(new OldNodeState(n)); 119 } 120 } 121 122 /** 123 * Constructs a new {@code MoveCommand} to move a collection of primitives. 124 * @param ds the dataset context for moving these primitives. Must not be null. 125 * @param objects The primitives to move 126 * @param start The starting position (northern/eastern) 127 * @param end The ending position (northern/eastern) 128 * @since 12759 129 */ 130 public MoveCommand(DataSet ds, Collection<OsmPrimitive> objects, EastNorth start, EastNorth end) { 131 this(Objects.requireNonNull(ds, "ds"), 132 Objects.requireNonNull(objects, "objects"), 133 Objects.requireNonNull(end, "end").getX() - Objects.requireNonNull(start, "start").getX(), 134 Objects.requireNonNull(end, "end").getY() - Objects.requireNonNull(start, "start").getY()); 135 startEN = start; 136 } 137 138 /** 139 * Constructs a new {@code MoveCommand} to move a collection of primitives. 140 * @param objects The primitives to move 141 * @param start The starting position (northern/eastern) 142 * @param end The ending position (northern/eastern) 143 */ 144 public MoveCommand(Collection<OsmPrimitive> objects, EastNorth start, EastNorth end) { 145 this(Objects.requireNonNull(objects, "objects").iterator().next().getDataSet(), objects, start, end); 146 } 147 148 /** 149 * Constructs a new {@code MoveCommand} to move a primitive. 150 * @param ds the dataset context for moving these primitives. Must not be null. 151 * @param p The primitive to move 152 * @param start The starting position (northern/eastern) 153 * @param end The ending position (northern/eastern) 154 * @since 12759 155 */ 156 public MoveCommand(DataSet ds, OsmPrimitive p, EastNorth start, EastNorth end) { 157 this(ds, Collections.singleton(Objects.requireNonNull(p, "p")), start, end); 158 } 159 160 /** 161 * Constructs a new {@code MoveCommand} to move a primitive. 162 * @param p The primitive to move 163 * @param start The starting position (northern/eastern) 164 * @param end The ending position (northern/eastern) 165 */ 166 public MoveCommand(OsmPrimitive p, EastNorth start, EastNorth end) { 167 this(Collections.singleton(Objects.requireNonNull(p, "p")), start, end); 168 } 169 170 /** 171 * Move the same set of objects again by the specified vector. The vectors 172 * are added together and so the resulting will be moved to the previous 173 * vector plus this one. 174 * 175 * The move is immediately executed and any undo will undo both vectors to 176 * the original position the objects had before first moving. 177 * 178 * @param x X difference movement. Coordinates are in northern/eastern 179 * @param y Y difference movement. Coordinates are in northern/eastern 180 */ 181 public void moveAgain(double x, double y) { 182 for (Node n : nodes) { 183 n.setEastNorth(n.getEastNorth().add(x, y)); 184 } 185 this.x += x; 186 this.y += y; 187 } 188 189 /** 190 * Move again to the specified coordinates. 191 * @param x X coordinate 192 * @param y Y coordinate 193 * @see #moveAgain 194 */ 195 public void moveAgainTo(double x, double y) { 196 moveAgain(x - this.x, y - this.y); 197 } 198 199 /** 200 * Change the displacement vector to have endpoint {@code currentEN}. 201 * starting point is startEN 202 * @param currentEN the new endpoint 203 */ 204 public void applyVectorTo(EastNorth currentEN) { 205 if (startEN == null) 206 return; 207 x = currentEN.getX() - startEN.getX(); 208 y = currentEN.getY() - startEN.getY(); 209 updateCoordinates(); 210 } 211 212 /** 213 * Changes base point of movement 214 * @param newDraggedStartPoint - new starting point after movement (where user clicks to start new drag) 215 */ 216 public void changeStartPoint(EastNorth newDraggedStartPoint) { 217 startEN = new EastNorth(newDraggedStartPoint.getX()-x, newDraggedStartPoint.getY()-y); 218 } 219 220 /** 221 * Save current displacement to restore in case of some problems 222 */ 223 public final void saveCheckpoint() { 224 backupX = x; 225 backupY = y; 226 } 227 228 /** 229 * Restore old displacement in case of some problems 230 */ 231 public void resetToCheckpoint() { 232 x = backupX; 233 y = backupY; 234 updateCoordinates(); 235 } 236 237 private void updateCoordinates() { 238 Iterator<OldNodeState> it = oldState.iterator(); 239 for (Node n : nodes) { 240 OldNodeState os = it.next(); 241 if (os.getEastNorth() != null) { 242 n.setEastNorth(os.getEastNorth().add(x, y)); 243 } 244 } 245 } 246 247 @Override 248 public boolean executeCommand() { 249 ensurePrimitivesAreInDataset(); 250 251 for (Node n : nodes) { 252 // in case #3892 happens again 253 if (n == null) 254 throw new AssertionError("null detected in node list"); 255 EastNorth en = n.getEastNorth(); 256 if (en != null) { 257 n.setEastNorth(en.add(x, y)); 258 n.setModified(true); 259 } 260 } 261 return true; 262 } 263 264 @Override 265 public void undoCommand() { 266 ensurePrimitivesAreInDataset(); 267 268 Iterator<OldNodeState> it = oldState.iterator(); 269 for (Node n : nodes) { 270 OldNodeState os = it.next(); 271 n.setCoor(os.getLatLon()); 272 n.setModified(os.isModified()); 273 } 274 } 275 276 @Override 277 public void fillModifiedData(Collection<OsmPrimitive> modified, Collection<OsmPrimitive> deleted, Collection<OsmPrimitive> added) { 278 for (OsmPrimitive osm : nodes) { 279 modified.add(osm); 280 } 281 } 282 283 @Override 284 public String getDescriptionText() { 285 return trn("Move {0} node", "Move {0} nodes", nodes.size(), nodes.size()); 286 } 287 288 @Override 289 public Icon getDescriptionIcon() { 290 return ImageProvider.get("data", "node"); 291 } 292 293 @Override 294 public Collection<Node> getParticipatingPrimitives() { 295 return nodes; 296 } 297 298 /** 299 * Gets the offset. 300 * @return The current offset. 301 */ 302 protected EastNorth getOffset() { 303 return new EastNorth(x, y); 304 } 305 306 @Override 307 public int hashCode() { 308 return Objects.hash(super.hashCode(), nodes, startEN, x, y, backupX, backupY, oldState); 309 } 310 311 @Override 312 public boolean equals(Object obj) { 313 if (this == obj) return true; 314 if (obj == null || getClass() != obj.getClass()) return false; 315 if (!super.equals(obj)) return false; 316 MoveCommand that = (MoveCommand) obj; 317 return Double.compare(that.x, x) == 0 && 318 Double.compare(that.y, y) == 0 && 319 Double.compare(that.backupX, backupX) == 0 && 320 Double.compare(that.backupY, backupY) == 0 && 321 Objects.equals(nodes, that.nodes) && 322 Objects.equals(startEN, that.startEN) && 323 Objects.equals(oldState, that.oldState); 324 } 325}