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.Objects;
008
009import org.openstreetmap.josm.data.coor.EastNorth;
010import org.openstreetmap.josm.data.osm.Node;
011import org.openstreetmap.josm.data.osm.OsmPrimitive;
012
013/**
014 * RotateCommand rotates a number of objects around their centre.
015 *
016 * @author Frederik Ramm
017 */
018public class RotateCommand extends TransformNodesCommand {
019
020    /**
021     * Pivot point
022     */
023    private final EastNorth pivot;
024
025    /**
026     * angle of rotation starting click to pivot
027     */
028    private final double startAngle;
029
030    /**
031     * computed rotation angle between starting click and current mouse pos
032     */
033    private double rotationAngle;
034
035    /**
036     * Creates a RotateCommand.
037     * Assign the initial object set, compute pivot point and inital rotation angle.
038     * @param objects objects to fetch nodes from
039     * @param currentEN cuurent eats/north
040     */
041    public RotateCommand(Collection<? extends OsmPrimitive> objects, EastNorth currentEN) {
042        super(objects);
043
044        pivot = getNodesCenter();
045        startAngle = getAngle(currentEN);
046        rotationAngle = 0.0;
047
048        handleEvent(currentEN);
049    }
050
051    /**
052     * Get angle between the horizontal axis and the line formed by the pivot and given point.
053     * @param currentEN cuurent eats/north
054     * @return angle between the horizontal axis and the line formed by the pivot and given point
055     **/
056    protected final double getAngle(EastNorth currentEN) {
057        if (pivot == null)
058            return 0.0; // should never happen by contract
059        return Math.atan2(currentEN.east()-pivot.east(), currentEN.north()-pivot.north());
060    }
061
062    /**
063     * Compute new rotation angle and transform nodes accordingly.
064     */
065    @Override
066    public final void handleEvent(EastNorth currentEN) {
067        double currentAngle = getAngle(currentEN);
068        rotationAngle = currentAngle - startAngle;
069        transformNodes();
070    }
071
072    /**
073     * Set the rotation angle.
074     * @param rotationAngle The rotate angle
075     */
076    protected void setRotationAngle(double rotationAngle) {
077        this.rotationAngle = rotationAngle;
078    }
079
080    /**
081     * Rotate nodes.
082     */
083    @Override
084    protected void transformNodes() {
085        double cosPhi = Math.cos(rotationAngle);
086        double sinPhi = Math.sin(rotationAngle);
087        for (Node n : nodes) {
088            EastNorth oldEastNorth = oldStates.get(n).getEastNorth();
089            double x = oldEastNorth.east() - pivot.east();
090            double y = oldEastNorth.north() - pivot.north();
091            // CHECKSTYLE.OFF: SingleSpaceSeparator
092            double nx =  cosPhi * x + sinPhi * y + pivot.east();
093            double ny = -sinPhi * x + cosPhi * y + pivot.north();
094            // CHECKSTYLE.ON: SingleSpaceSeparator
095            n.setEastNorth(new EastNorth(nx, ny));
096        }
097    }
098
099    @Override
100    public String getDescriptionText() {
101        return trn("Rotate {0} node", "Rotate {0} nodes", nodes.size(), nodes.size());
102    }
103
104    @Override
105    public int hashCode() {
106        return Objects.hash(super.hashCode(), pivot, startAngle, rotationAngle);
107    }
108
109    @Override
110    public boolean equals(Object obj) {
111        if (this == obj) return true;
112        if (obj == null || getClass() != obj.getClass()) return false;
113        if (!super.equals(obj)) return false;
114        RotateCommand that = (RotateCommand) obj;
115        return Double.compare(that.startAngle, startAngle) == 0 &&
116                Double.compare(that.rotationAngle, rotationAngle) == 0 &&
117                Objects.equals(pivot, that.pivot);
118    }
119}