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}