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