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