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 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     */
069    public MoveCommand(Node node, LatLon position) {
070        this(Collections.singleton((OsmPrimitive) node), node.getEastNorth().sub(Projections.project(position)));
071    }
072
073    /**
074     * Constructs a new {@code MoveCommand} to move a collection of primitives.
075     * @param objects The primitives to move
076     * @param offset The movement vector
077     */
078    public MoveCommand(Collection<OsmPrimitive> objects, EastNorth offset) {
079        this(objects, offset.getX(), offset.getY());
080    }
081
082    /**
083     * Constructs a new {@code MoveCommand} and assign the initial object set and movement vector.
084     * @param objects The primitives to move
085     * @param x X difference movement. Coordinates are in northern/eastern
086     * @param y Y difference movement. Coordinates are in northern/eastern
087     */
088    public MoveCommand(Collection<OsmPrimitive> objects, double x, double y) {
089        startEN = null;
090        saveCheckpoint(); // (0,0) displacement will be saved
091        this.x = x;
092        this.y = y;
093        this.nodes = AllNodesVisitor.getAllNodes(objects);
094        for (Node n : this.nodes) {
095            oldState.add(new OldNodeState(n));
096        }
097    }
098
099    public MoveCommand(Collection<OsmPrimitive> objects, EastNorth start, EastNorth end) {
100        this(objects, end.getX()-start.getX(), end.getY()-start.getY());
101        startEN =  start;
102    }
103
104    public MoveCommand(OsmPrimitive p, EastNorth start, EastNorth end) {
105        this(Collections.singleton(p), end.getX()-start.getX(), end.getY()-start.getY());
106        startEN =  start;
107    }
108
109    /**
110     * Move the same set of objects again by the specified vector. The vectors
111     * are added together and so the resulting will be moved to the previous
112     * vector plus this one.
113     *
114     * The move is immediately executed and any undo will undo both vectors to
115     * the original position the objects had before first moving.
116     *
117     * @param x X difference movement. Coordinates are in northern/eastern
118     * @param y Y difference movement. Coordinates are in northern/eastern
119     */
120    public void moveAgain(double x, double y) {
121        for (Node n : nodes) {
122            n.setEastNorth(n.getEastNorth().add(x, y));
123        }
124        this.x += x;
125        this.y += y;
126    }
127
128    public void moveAgainTo(double x, double y) {
129        moveAgain(x - this.x, y - this.y);
130    }
131
132    /**
133     * Change the displacement vector to have endpoint @param currentEN
134     * starting point is  startEN
135     */
136    public void applyVectorTo(EastNorth currentEN) {
137        if (startEN == null)
138            return;
139        x = currentEN.getX() - startEN.getX();
140        y = currentEN.getY() - startEN.getY();
141        updateCoordinates();
142    }
143
144    /**
145     * Changes base point of movement
146     * @param newDraggedStartPoint - new starting point after movement (where user clicks to start new drag)
147     */
148    public void changeStartPoint(EastNorth newDraggedStartPoint) {
149        startEN = new EastNorth(newDraggedStartPoint.getX()-x, newDraggedStartPoint.getY()-y);
150    }
151
152    /**
153     * Save curent displacement to restore in case of some problems
154     */
155    public final void saveCheckpoint() {
156        backupX = x;
157        backupY = y;
158    }
159
160    /**
161     * Restore old displacement in case of some problems
162     */
163    public void resetToCheckpoint() {
164        x = backupX;
165        y = backupY;
166        updateCoordinates();
167    }
168
169    private void updateCoordinates() {
170        Iterator<OldNodeState> it = oldState.iterator();
171        for (Node n : nodes) {
172            OldNodeState os = it.next();
173            if (os.eastNorth != null) {
174                n.setEastNorth(os.eastNorth.add(x, y));
175            }
176        }
177    }
178
179    @Override
180    public boolean executeCommand() {
181        for (Node n : nodes) {
182            // in case #3892 happens again
183            if (n == null)
184                throw new AssertionError("null detected in node list");
185            EastNorth en = n.getEastNorth();
186            if (en != null) {
187                n.setEastNorth(en.add(x, y));
188                n.setModified(true);
189            }
190        }
191        return true;
192    }
193
194    @Override
195    public void undoCommand() {
196        Iterator<OldNodeState> it = oldState.iterator();
197        for (Node n : nodes) {
198            OldNodeState os = it.next();
199            n.setCoor(os.latlon);
200            n.setModified(os.modified);
201        }
202    }
203
204    @Override
205    public void fillModifiedData(Collection<OsmPrimitive> modified, Collection<OsmPrimitive> deleted, Collection<OsmPrimitive> added) {
206        for (OsmPrimitive osm : nodes) {
207            modified.add(osm);
208        }
209    }
210
211    @Override
212    public String getDescriptionText() {
213        return trn("Move {0} node", "Move {0} nodes", nodes.size(), nodes.size());
214    }
215
216    @Override
217    public Icon getDescriptionIcon() {
218        return ImageProvider.get("data", "node");
219    }
220
221    @Override
222    public Collection<Node> getParticipatingPrimitives() {
223        return nodes;
224    }
225}