Ticket #5652: patch

File patch, 22.4 KB (added by Olivier Croquette <ocroquette@…>, 15 years ago)

patch

  • core/src/org/openstreetmap/josm/actions/mapmode/SelectAction.java

     
    2626import org.openstreetmap.josm.command.Command;
    2727import org.openstreetmap.josm.command.MoveCommand;
    2828import org.openstreetmap.josm.command.RotateCommand;
     29import org.openstreetmap.josm.command.ScaleCommand;
    2930import org.openstreetmap.josm.command.SequenceCommand;
    3031import org.openstreetmap.josm.data.coor.EastNorth;
    3132import org.openstreetmap.josm.data.osm.DataSet;
     
    6061public class SelectAction extends MapMode implements SelectionEnded {
    6162    //static private final Logger logger = Logger.getLogger(SelectAction.class.getName());
    6263
    63     enum Mode { move, rotate, select }
     64    enum Mode { move, rotate, scale, select }
    6465    private Mode mode = null;
    6566    private SelectionManager selectionManager;
    6667
     
    7879    private Cursor oldCursor;
    7980
    8081    /**
    81      * The position of the mouse before the user moves a node.
     82     * The position of the mouse before the user starts to drag it while pressing a button.
    8283     */
    83     private Point mousePos;
     84    private Point startingDraggingPos;
    8485
    8586    /**
     87     * The last known position of the mouse.
     88     */
     89    private Point lastMousePos;
     90
     91    /**
    8692     * The time of the user mouse down event.
    8793     */
    8894    private long mouseDownTime = 0;
     
    105111     * @param mapFrame The MapFrame this action belongs to.
    106112     */
    107113    public SelectAction(MapFrame mapFrame) {
    108         super(tr("Select"), "move/move", tr("Select, move and rotate objects"),
     114        super(tr("Select"), "move/move", tr("Select, move, scale and rotate objects"),
    109115                Shortcut.registerShortcut("mapmode:select", tr("Mode: {0}", tr("Select")), KeyEvent.VK_S, Shortcut.GROUP_EDIT),
    110116                mapFrame,
    111117                getCursor("normal", "selection", Cursor.DEFAULT_CURSOR));
     
    169175        // do not count anything as a move if it lasts less than 100 milliseconds.
    170176        if ((mode == Mode.move) && (System.currentTimeMillis() - mouseDownTime < initialMoveDelay)) return;
    171177
    172         if(mode != Mode.rotate) // button is pressed in rotate mode
     178        if(mode != Mode.rotate && mode != Mode.scale) // button is pressed in rotate mode
    173179            if ((e.getModifiersEx() & MouseEvent.BUTTON1_DOWN_MASK) == 0)
    174180                return;
    175181
     
    177183            setCursor(Cursor.getPredefinedCursor(Cursor.MOVE_CURSOR));
    178184        }
    179185
     186        if ( startingDraggingPos == null )
     187                startingDraggingPos = new Point(e.getX(), e.getY());
     188       
    180189        if (!initialMoveThresholdExceeded) {
    181             int dxp = mousePos.x - e.getX();
    182             int dyp = mousePos.y - e.getY();
     190            int dxp = lastMousePos.x - e.getX();
     191            int dyp = lastMousePos.y - e.getY();
    183192            int dp = (int) Math.sqrt(dxp*dxp+dyp*dyp);
    184193            if (dp < initialMoveThreshold) return;
    185194            initialMoveThresholdExceeded = true;
    186195        }
    187196
    188         EastNorth mouseEN = mv.getEastNorth(e.getX(), e.getY());
    189         EastNorth mouseStartEN = mv.getEastNorth(mousePos.x, mousePos.y);
    190         double dx = mouseEN.east() - mouseStartEN.east();
    191         double dy = mouseEN.north() - mouseStartEN.north();
     197        EastNorth currentEN = mv.getEastNorth(e.getX(), e.getY());
     198        EastNorth lastEN = mv.getEastNorth(lastMousePos.x, lastMousePos.y);
     199        EastNorth startEN = mv.getEastNorth(startingDraggingPos.x, startingDraggingPos.y);
     200        double dx = currentEN.east() - lastEN.east();
     201        double dy = currentEN.north() - lastEN.north();
    192202        if (dx == 0 && dy == 0)
    193203            return;
    194204
     
    210220            virtualWays.clear();
    211221            virtualNode = null;
    212222        } else {
    213             // Currently we support moving and rotating, which do not affect relations.
     223            // Currently we support only transformations which do not affect relations.
    214224            // So don't add them in the first place to make handling easier
    215225            Collection<OsmPrimitive> selection = getCurrentDataSet().getSelectedNodesAndWays();
    216226            Collection<Node> affectedNodes = AllNodesVisitor.getAllNodes(selection);
    217227
    218             // when rotating, having only one node makes no sense - quit silently
    219             if (mode == Mode.rotate && affectedNodes.size() < 2)
     228            // for these transformations, having only one node makes no sense - quit silently
     229            if (affectedNodes.size() < 2 && ( mode == Mode.rotate || mode == Mode.scale ) )
    220230                return;
    221231
    222232            Command c = !Main.main.undoRedo.commands.isEmpty()
     
    250260                    }
    251261                }
    252262            } else if (mode == Mode.rotate) {
    253                 if (c instanceof RotateCommand && affectedNodes.equals(((RotateCommand)c).getRotatedNodes())) {
    254                     ((RotateCommand)c).rotateAgain(mouseStartEN, mouseEN);
     263                if (c instanceof RotateCommand && affectedNodes.equals(((RotateCommand)c).getTransformedNodes())) {
     264                    ((RotateCommand)c).handleEvent(currentEN);
    255265                } else {
    256                     Main.main.undoRedo.add(new RotateCommand(selection, mouseStartEN, mouseEN));
     266                    Main.main.undoRedo.add(new RotateCommand(selection, currentEN));
    257267                }
     268            } else if (mode == Mode.scale) {
     269                if (c instanceof ScaleCommand && affectedNodes.equals(((ScaleCommand)c).getTransformedNodes())) {
     270                    ((ScaleCommand)c).handleEvent(currentEN);
     271                } else {
     272                    Main.main.undoRedo.add(new ScaleCommand(selection, currentEN));
     273                }
    258274            }
    259275        }
    260276
    261277        mv.repaint();
    262         mousePos = e.getPoint();
     278        if ( mode != Mode.scale )
     279        lastMousePos = e.getPoint();
    263280
    264281        didMouseDrag = true;
    265282    }
     
    267284    @Override public void mouseMoved(MouseEvent e) {
    268285        // Mac OSX simulates with  ctrl + mouse 1  the second mouse button hence no dragging events get fired.
    269286        //
    270         if ((Main.platform instanceof PlatformHookOsx) && mode == Mode.rotate) {
     287        if ((Main.platform instanceof PlatformHookOsx) && (mode == Mode.rotate || mode == Mode.scale) ) {
    271288            mouseDragged(e);
    272289        }
    273290    }
     
    418435
    419436        boolean ctrl = (e.getModifiers() & ActionEvent.CTRL_MASK) != 0;
    420437        boolean shift = (e.getModifiers() & ActionEvent.SHIFT_MASK) != 0;
     438        boolean alt = (e.getModifiers() & ActionEvent.ALT_MASK) != 0;
    421439
    422440        // We don't want to change to draw tool if the user tries to (de)select
    423441        // stuff but accidentally clicks in an empty area when selection is empty
     
    425443        didMouseDrag = false;
    426444        initialMoveThresholdExceeded = false;
    427445        mouseDownTime = System.currentTimeMillis();
    428         mousePos = e.getPoint();
     446        lastMousePos = e.getPoint();
    429447
    430448        Collection<OsmPrimitive> c = MapView.asColl(
    431449                mv.getNearestNodeOrWay(e.getPoint(), OsmPrimitive.isSelectablePredicate, false));
     
    442460            // Mode.rotate redraws here
    443461            setCursor(ImageProvider.getCursor("rotate", null));
    444462            mv.repaint();
     463        } else if (alt && ctrl) {
     464                mode = Mode.scale;
     465
     466                if (getCurrentDataSet().getSelected().isEmpty()) {
     467                    getCurrentDataSet().setSelected(c);
     468                }
     469
     470                // Mode.select redraws when selectPrims is called
     471                // Mode.move   redraws when mouseDragged is called
     472                // Mode.scale redraws here
     473                // TODO: mouse cursor for scaling
     474                setCursor(ImageProvider.getCursor("rotate", null));
     475                mv.repaint();
    445476        } else if (!c.isEmpty()) {
    446477            mode = Mode.move;
    447478
     
    468499        if(!mv.isActiveLayerVisible())
    469500            return;
    470501
     502        startingDraggingPos = null;
     503
    471504        restoreCursor();
    472505        if (mode == Mode.select) {
    473506            selectionManager.unregister(mv);
     
    487520
    488521                // do nothing if the click was to short to be recognized as a drag,
    489522                // but the release position is farther than 10px away from the press position
    490                 if (mousePos.distanceSq(e.getPoint())<100) {
     523                if (lastMousePos.distanceSq(e.getPoint())<100) {
    491524                    selectPrims(cyclePrims(cycleList, e), e, true, false);
    492525
    493526                    // If the user double-clicked a node, change to draw mode
     
    678711            return tr("Release the mouse button to stop moving. Ctrl to merge with nearest node.");
    679712        else if (mode == Mode.rotate)
    680713            return tr("Release the mouse button to stop rotating.");
     714        else if (mode == Mode.scale)
     715            return tr("Release the mouse button to stop scaling.");
    681716        else
    682             return tr("Move objects by dragging; Shift to add to selection (Ctrl to toggle); Shift-Ctrl to rotate selected; or change selection");
     717            return tr("Move objects by dragging; Shift to add to selection (Ctrl to toggle); Shift-Ctrl to rotate selected; Alt-Ctrl to scale selected; or change selection");
    683718    }
    684719
    685720    @Override public boolean layerIsSupported(Layer l) {
  • core/src/org/openstreetmap/josm/command/TransformNodesCommand.java

     
     1package org.openstreetmap.josm.command;
     2
     3import static org.openstreetmap.josm.tools.I18n.trn;
     4
     5import java.awt.Point;
     6
     7import java.util.Collection;
     8import java.util.HashMap;
     9import java.util.LinkedList;
     10import java.util.Map;
     11
     12import javax.swing.JLabel;
     13
     14import org.openstreetmap.josm.data.coor.EastNorth;
     15import org.openstreetmap.josm.data.coor.LatLon;
     16import org.openstreetmap.josm.data.osm.Node;
     17import org.openstreetmap.josm.data.osm.OsmPrimitive;
     18import org.openstreetmap.josm.data.osm.visitor.AllNodesVisitor;
     19import org.openstreetmap.josm.tools.ImageProvider;
     20
     21/**
     22 * Abstract class with common services for nodes rotation and scaling commands.
     23 *
     24 * @author Olivier Croquette <ocroquette@free.fr>
     25 */
     26public abstract class TransformNodesCommand extends Command {
     27
     28        /**
     29         * The nodes to transform.
     30         */
     31        protected Collection<Node> nodes = new LinkedList<Node>();
     32
     33        /**
     34         * Small helper for holding the interesting part of the old data state of the
     35         * nodes.
     36         */
     37        public static class OldState {
     38                LatLon latlon;
     39                EastNorth eastNorth;
     40                boolean modified;
     41        }
     42
     43        /**
     44         * List of all old states of the nodes.
     45         */
     46        protected Map<Node, OldState> oldStates = new HashMap<Node, OldState>();
     47
     48        /**
     49         * Stores the state of the nodes before the command.
     50         */
     51        protected void storeOldState() {
     52                for (Node n : this.nodes) {
     53                        OldState os = new OldState();
     54                        os.latlon = new LatLon(n.getCoor());
     55                        os.eastNorth = n.getEastNorth();
     56                        os.modified = n.isModified();
     57                        oldStates.put(n, os);
     58                }
     59        }
     60
     61
     62        /**
     63         * Creates a TransformNodesObject.
     64         * Find out the impacted nodes and store their initial state.
     65         */
     66        public TransformNodesCommand(Collection<OsmPrimitive> objects) {
     67                this.nodes = AllNodesVisitor.getAllNodes(objects);
     68                storeOldState();
     69        }
     70
     71        /**
     72         * Handling of a mouse event (e.g. dragging event).
     73     * @param currentEN the current world position of the mouse
     74         */
     75        public abstract void handleEvent(EastNorth currentEN);
     76
     77        /**
     78         * Implementation for the nodes transformation.
     79         * No parameters are given here, you should handle the user input in handleEvent()
     80         * and store it internally.
     81         */
     82        protected abstract void transformNodes();
     83       
     84        /**
     85         * Finally apply the transformation of the nodes.
     86         * This is called when the user is happy with the current state of the command
     87         * and its effects.
     88         */
     89        @Override
     90        public boolean executeCommand() {
     91                transformNodes();
     92                flagNodesAsModified();
     93                return true;
     94        }
     95
     96        /**
     97         * Flag all nodes as modified.
     98         */
     99        public void flagNodesAsModified() {
     100                for (Node n : nodes) {
     101                        n.setModified(true);
     102                }
     103        }
     104
     105        /**
     106         * Restore the state of the nodes from the backup.
     107         */
     108        @Override
     109        public void undoCommand() {
     110                for (Node n : nodes) {
     111                        OldState os = oldStates.get(n);
     112                        n.setCoor(os.latlon);
     113                        n.setModified(os.modified);
     114                }
     115        }
     116
     117        @Override
     118        public void fillModifiedData(Collection<OsmPrimitive> modified, Collection<OsmPrimitive> deleted, Collection<OsmPrimitive> added) {
     119                for (OsmPrimitive osm : nodes) {
     120                        modified.add(osm);
     121                }
     122        }
     123
     124        @Override
     125        public JLabel getDescription() {
     126                return new JLabel(trn("Transform {0} node", "Transform {0} nodes", nodes.size(), nodes.size()), ImageProvider.get("data", "node"), JLabel.HORIZONTAL);
     127        }
     128
     129        /**
     130         * Get the nodes with the current transformation applied.
     131         */
     132        public Collection<Node> getTransformedNodes() {
     133                return nodes;
     134        }
     135
     136        /**
     137         * Get the center of the nodes under modification.
     138         * It's just the barycenter.
     139         */
     140        public EastNorth getNodesCenter() {
     141                EastNorth sum = new EastNorth(0,0);
     142
     143                for (Node n : nodes ) {
     144                        EastNorth en = n.getEastNorth();
     145                        sum = sum.add(en.east(), en.north());
     146                }
     147                return new EastNorth(sum.east()/this.nodes.size(), sum.north()/this.nodes.size());
     148
     149        }
     150}
  • core/src/org/openstreetmap/josm/command/ScaleCommand.java

     
     1package org.openstreetmap.josm.command;
     2
     3import static org.openstreetmap.josm.tools.I18n.trn;
     4
     5import java.util.Collection;
     6
     7import javax.swing.JLabel;
     8
     9import org.openstreetmap.josm.data.coor.EastNorth;
     10import org.openstreetmap.josm.data.osm.Node;
     11import org.openstreetmap.josm.data.osm.OsmPrimitive;
     12import org.openstreetmap.josm.tools.ImageProvider;
     13
     14public class ScaleCommand extends TransformNodesCommand {
     15        /**
     16         * Pivot point
     17         */
     18        private EastNorth pivot;
     19
     20        /**
     21         * Current scaling factor applied
     22         */
     23        private double scalingFactor;
     24
     25        /**
     26         * World position of the mouse when the user started the command.
     27         *
     28         */
     29        EastNorth startEN = null;
     30       
     31        /**
     32         * Creates a ScaleCommand.
     33         * Assign the initial object set, compute pivot point.
     34         * Computation of pivot point is done by the same rules that are used in
     35         * the "align nodes in circle" action.
     36         */
     37        public ScaleCommand(Collection<OsmPrimitive> objects, EastNorth currentEN) {
     38                super(objects);
     39
     40                pivot = getNodesCenter();
     41
     42                // We remember the very first position of the mouse for this action.
     43                // Note that SelectAction will keep the same ScaleCommand when the user
     44                // releases the button and presses it again with the same modifiers.
     45                // The very first point of this operation is stored here.
     46                startEN   = currentEN;
     47
     48                handleEvent(currentEN);
     49        }
     50
     51        /**
     52         * Compute new scaling factor and transform nodes accordingly.
     53         */
     54        @Override
     55        public void handleEvent(EastNorth currentEN) {
     56                double startAngle = Math.atan2(startEN.east()-pivot.east(), startEN.north()-pivot.north());
     57                double endAngle = Math.atan2(currentEN.east()-pivot.east(), currentEN.north()-pivot.north());
     58                double startDistance = pivot.distance(startEN);
     59                double currentDistance = pivot.distance(currentEN);
     60                scalingFactor = Math.cos(startAngle-endAngle) * currentDistance / startDistance;
     61                transformNodes();
     62        }
     63
     64
     65        /**
     66         * Scale nodes.
     67         */
     68        @Override
     69        protected void transformNodes() {
     70                // scalingFactor = 2.0;
     71                for (Node n : nodes) {
     72                        EastNorth oldEastNorth = oldStates.get(n).eastNorth;
     73                        double dx = oldEastNorth.east() - pivot.east();
     74                        double dy = oldEastNorth.north() - pivot.north();
     75                        double nx = pivot.east() + scalingFactor * dx;
     76                        double ny = pivot.north() + scalingFactor * dy;
     77                        n.setEastNorth(new EastNorth(nx, ny));
     78                }
     79        }
     80
     81        @Override public JLabel getDescription() {
     82                return new JLabel(trn("Scale {0} node", "Scale {0} nodes", nodes.size(), nodes.size()), ImageProvider.get("data", "node"), JLabel.HORIZONTAL);
     83        }
     84
     85}
  • core/src/org/openstreetmap/josm/command/RotateCommand.java

     
    1919/**
    2020 * RotateCommand rotates a number of objects around their centre.
    2121 *
    22  * @author Frederik Ramm <frederik@remote.org>
    2322 */
    24 public class RotateCommand extends Command {
     23public class RotateCommand extends TransformNodesCommand {
    2524
    2625    /**
    27      * The objects to rotate.
     26     * Pivot point
    2827     */
    29     private Collection<Node> nodes = new LinkedList<Node>();
    30 
    31     /**
    32      * pivot point
    33      */
    3428    private EastNorth pivot;
    3529
    36     /**
    37      * Small helper for holding the interesting part of the old data state of the
    38      * objects.
    39      */
    40     public static class OldState {
    41         LatLon latlon;
    42         EastNorth eastNorth;
    43         boolean modified;
    44     }
     30        /**
     31         * World position of the mouse when the user started the command.
     32         *
     33         */
     34        EastNorth startEN = null;
    4535
    4636    /**
    4737     * angle of rotation starting click to pivot
    4838     */
    49     private double startAngle;
     39    private double startAngle = 0.0;
    5040
    5141    /**
    5242     * computed rotation angle between starting click and current mouse pos
    5343     */
    54     private double rotationAngle;
     44    private double rotationAngle = 0.0;
    5545
    5646    /**
    57      * List of all old states of the objects.
    58      */
    59     private Map<Node, OldState> oldState = new HashMap<Node, OldState>();
    60 
    61     /**
    6247     * Creates a RotateCommand.
    63      * Assign the initial object set, compute pivot point and rotation angle.
    64      * Computation of pivot point is done by the same rules that are used in
    65      * the "align nodes in circle" action.
     48     * Assign the initial object set, compute pivot point and inital rotation angle.
    6649     */
    67     public RotateCommand(Collection<OsmPrimitive> objects, EastNorth start, EastNorth end) {
     50    public RotateCommand(Collection<OsmPrimitive> objects, EastNorth currentEN) {
     51                super(objects);
    6852
    69         this.nodes = AllNodesVisitor.getAllNodes(objects);
    70         pivot = new EastNorth(0,0);
     53                pivot = getNodesCenter();
    7154
    72         for (Node n : this.nodes) {
    73             OldState os = new OldState();
    74             os.latlon = new LatLon(n.getCoor());
    75             os.eastNorth = n.getEastNorth();
    76             os.modified = n.isModified();
    77             oldState.put(n, os);
    78             pivot = pivot.add(os.eastNorth.east(), os.eastNorth.north());
    79         }
    80         pivot = new EastNorth(pivot.east()/this.nodes.size(), pivot.north()/this.nodes.size());
     55                // We remember the very first position of the mouse for this action.
     56                // Note that SelectAction will keep the same ScaleCommand when the user
     57                // releases the button and presses it again with the same modifiers.
     58                // The very first point of this operation is stored here.
     59                startEN   = currentEN;
    8160
    82         rotationAngle = Math.PI/2;
    83         rotateAgain(start, end);
    84     }
     61                startAngle = getAngle(currentEN);
     62        rotationAngle = 0.0;
    8563
     64                handleEvent(currentEN);
     65        }
     66   
    8667    /**
    87      * Rotate the same set of objects again, by the angle between given
    88      * start and end nodes. Internally this is added to the existing
    89      * rotation so a later undo will undo the whole rotation.
    90      */
    91     public void rotateAgain(EastNorth start, EastNorth end) {
    92         // compute angle
    93         startAngle = Math.atan2(start.east()-pivot.east(), start.north()-pivot.north());
    94         double endAngle = Math.atan2(end.east()-pivot.east(), end.north()-pivot.north());
    95         rotationAngle += startAngle - endAngle;
    96         rotateNodes(false);
     68     * Get angle between the horizontal axis and the line formed by the pivot and give points.
     69     **/
     70    protected double getAngle(EastNorth currentEN) {
     71        if ( pivot == null )
     72                return 0.0; // should never happen by contract
     73        return Math.atan2(currentEN.east()-pivot.east(), currentEN.north()-pivot.north());
    9774    }
    9875
    99     /**
    100      * Helper for actually rotationg the nodes.
    101      * @param setModified - true if rotated nodes should be flagged "modified"
    102      */
    103     private void rotateNodes(boolean setModified) {
     76        /**
     77         * Compute new rotation angle and transform nodes accordingly.
     78         */
     79    @Override
     80    public void handleEvent(EastNorth currentEN) {
     81        double currentAngle = getAngle(currentEN);
     82        rotationAngle = currentAngle - startAngle;
     83        transformNodes();
     84    }
     85
     86        /**
     87         * Rotate nodes.
     88         */
     89    @Override
     90    protected void transformNodes() {
    10491        for (Node n : nodes) {
    10592            double cosPhi = Math.cos(rotationAngle);
    10693            double sinPhi = Math.sin(rotationAngle);
    107             EastNorth oldEastNorth = oldState.get(n).eastNorth;
     94            EastNorth oldEastNorth = oldStates.get(n).eastNorth;
    10895            double x = oldEastNorth.east() - pivot.east();
    10996            double y = oldEastNorth.north() - pivot.north();
    110             double nx =  sinPhi * x + cosPhi * y + pivot.east();
    111             double ny = -cosPhi * x + sinPhi * y + pivot.north();
     97            double nx =  cosPhi * x + sinPhi * y + pivot.east();
     98            double ny = -sinPhi * x + cosPhi * y + pivot.north();
    11299            n.setEastNorth(new EastNorth(nx, ny));
    113             if (setModified) {
    114                 n.setModified(true);
    115             }
    116100        }
    117101    }
    118102
    119     @Override public boolean executeCommand() {
    120         rotateNodes(true);
    121         return true;
    122     }
    123 
    124     @Override public void undoCommand() {
    125         for (Node n : nodes) {
    126             OldState os = oldState.get(n);
    127             n.setCoor(os.latlon);
    128             n.setModified(os.modified);
    129         }
    130     }
    131 
    132     @Override public void fillModifiedData(Collection<OsmPrimitive> modified, Collection<OsmPrimitive> deleted, Collection<OsmPrimitive> added) {
    133         for (OsmPrimitive osm : nodes) {
    134             modified.add(osm);
    135         }
    136     }
    137 
    138     @Override public JLabel getDescription() {
     103    @Override
     104    public JLabel getDescription() {
    139105        return new JLabel(trn("Rotate {0} node", "Rotate {0} nodes", nodes.size(), nodes.size()), ImageProvider.get("data", "node"), JLabel.HORIZONTAL);
    140106    }
    141107
    142     public Collection<Node> getRotatedNodes() {
    143         return nodes;
    144     }
    145108}