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 EastNorth eastNorth = n.getEastNorth(); 184 if (eastNorth != null) { 185 n.setEastNorth(eastNorth.add(x, y)); 186 } 187 } 188 this.x += x; 189 this.y += y; 190 } 191 192 /** 193 * Move again to the specified coordinates. 194 * @param x X coordinate 195 * @param y Y coordinate 196 * @see #moveAgain 197 */ 198 public void moveAgainTo(double x, double y) { 199 moveAgain(x - this.x, y - this.y); 200 } 201 202 /** 203 * Change the displacement vector to have endpoint {@code currentEN}. 204 * starting point is startEN 205 * @param currentEN the new endpoint 206 */ 207 public void applyVectorTo(EastNorth currentEN) { 208 if (startEN == null) 209 return; 210 x = currentEN.getX() - startEN.getX(); 211 y = currentEN.getY() - startEN.getY(); 212 updateCoordinates(); 213 } 214 215 /** 216 * Changes base point of movement 217 * @param newDraggedStartPoint - new starting point after movement (where user clicks to start new drag) 218 */ 219 public void changeStartPoint(EastNorth newDraggedStartPoint) { 220 startEN = new EastNorth(newDraggedStartPoint.getX()-x, newDraggedStartPoint.getY()-y); 221 } 222 223 /** 224 * Save current displacement to restore in case of some problems 225 */ 226 public final void saveCheckpoint() { 227 backupX = x; 228 backupY = y; 229 } 230 231 /** 232 * Restore old displacement in case of some problems 233 */ 234 public void resetToCheckpoint() { 235 x = backupX; 236 y = backupY; 237 updateCoordinates(); 238 } 239 240 private void updateCoordinates() { 241 Iterator<OldNodeState> it = oldState.iterator(); 242 for (Node n : nodes) { 243 OldNodeState os = it.next(); 244 if (os.getEastNorth() != null) { 245 n.setEastNorth(os.getEastNorth().add(x, y)); 246 } 247 } 248 } 249 250 @Override 251 public boolean executeCommand() { 252 ensurePrimitivesAreInDataset(); 253 254 for (Node n : nodes) { 255 // in case #3892 happens again 256 if (n == null) 257 throw new AssertionError("null detected in node list"); 258 EastNorth en = n.getEastNorth(); 259 if (en != null) { 260 n.setEastNorth(en.add(x, y)); 261 n.setModified(true); 262 } 263 } 264 return true; 265 } 266 267 @Override 268 public void undoCommand() { 269 ensurePrimitivesAreInDataset(); 270 271 Iterator<OldNodeState> it = oldState.iterator(); 272 for (Node n : nodes) { 273 OldNodeState os = it.next(); 274 n.setCoor(os.getLatLon()); 275 n.setModified(os.isModified()); 276 } 277 } 278 279 @Override 280 public void fillModifiedData(Collection<OsmPrimitive> modified, Collection<OsmPrimitive> deleted, Collection<OsmPrimitive> added) { 281 for (OsmPrimitive osm : nodes) { 282 modified.add(osm); 283 } 284 } 285 286 @Override 287 public String getDescriptionText() { 288 return trn("Move {0} node", "Move {0} nodes", nodes.size(), nodes.size()); 289 } 290 291 @Override 292 public Icon getDescriptionIcon() { 293 return ImageProvider.get("data", "node"); 294 } 295 296 @Override 297 public Collection<Node> getParticipatingPrimitives() { 298 return nodes; 299 } 300 301 /** 302 * Gets the offset. 303 * @return The current offset. 304 */ 305 protected EastNorth getOffset() { 306 return new EastNorth(x, y); 307 } 308 309 @Override 310 public int hashCode() { 311 return Objects.hash(super.hashCode(), nodes, startEN, x, y, backupX, backupY, oldState); 312 } 313 314 @Override 315 public boolean equals(Object obj) { 316 if (this == obj) return true; 317 if (obj == null || getClass() != obj.getClass()) return false; 318 if (!super.equals(obj)) return false; 319 MoveCommand that = (MoveCommand) obj; 320 return Double.compare(that.x, x) == 0 && 321 Double.compare(that.y, y) == 0 && 322 Double.compare(that.backupX, backupX) == 0 && 323 Double.compare(that.backupY, backupY) == 0 && 324 Objects.equals(nodes, that.nodes) && 325 Objects.equals(startEN, that.startEN) && 326 Objects.equals(oldState, that.oldState); 327 } 328}