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