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}