Index: trunk/src/org/openstreetmap/josm/actions/mapmode/SelectAction.java
===================================================================
--- trunk/src/org/openstreetmap/josm/actions/mapmode/SelectAction.java	(revision 3700)
+++ trunk/src/org/openstreetmap/josm/actions/mapmode/SelectAction.java	(revision 3702)
@@ -27,4 +27,5 @@
 import org.openstreetmap.josm.command.MoveCommand;
 import org.openstreetmap.josm.command.RotateCommand;
+import org.openstreetmap.josm.command.ScaleCommand;
 import org.openstreetmap.josm.command.SequenceCommand;
 import org.openstreetmap.josm.data.coor.EastNorth;
@@ -61,31 +62,30 @@
     //static private final Logger logger = Logger.getLogger(SelectAction.class.getName());
 
-    enum Mode { move, rotate, select }
+    enum Mode { move, rotate, scale, select }
+    
     private Mode mode = null;
     private SelectionManager selectionManager;
-
     private boolean cancelDrawMode = false;
     private boolean didMouseDrag = false;
-
     /**
      * The component this SelectAction is associated with.
      */
     private final MapView mv;
-
     /**
      * The old cursor before the user pressed the mouse button.
      */
     private Cursor oldCursor;
-
-    /**
-     * The position of the mouse before the user moves a node.
-     */
-    private Point mousePos;
-
+    /**
+     * The position of the mouse before the user starts to drag it while pressing a button.
+     */
+    private Point startingDraggingPos;
+    /**
+     * The last known position of the mouse.
+     */
+    private Point lastMousePos;
     /**
      * The time of the user mouse down event.
      */
     private long mouseDownTime = 0;
-
     /**
      * The time which needs to pass between click and release before something
@@ -93,5 +93,4 @@
      */
     private int initialMoveDelay;
-
     /**
      * The screen distance which needs to be travelled before something
@@ -106,5 +105,5 @@
      */
     public SelectAction(MapFrame mapFrame) {
-        super(tr("Select"), "move/move", tr("Select, move and rotate objects"),
+        super(tr("Select"), "move/move", tr("Select, move, scale and rotate objects"),
                 Shortcut.registerShortcut("mapmode:select", tr("Mode: {0}", tr("Select")), KeyEvent.VK_S, Shortcut.GROUP_EDIT),
                 mapFrame,
@@ -113,6 +112,6 @@
         putValue("help", "Action/Move/Move");
         selectionManager = new SelectionManager(this, false, mv);
-        initialMoveDelay = Main.pref.getInteger("edit.initial-move-delay",200);
-        initialMoveThreshold = Main.pref.getInteger("edit.initial-move-threshold",5);
+        initialMoveDelay = Main.pref.getInteger("edit.initial-move-delay", 200);
+        initialMoveThreshold = Main.pref.getInteger("edit.initial-move-threshold", 5);
     }
 
@@ -139,5 +138,6 @@
     }
 
-    @Override public void enterMode() {
+    @Override
+    public void enterMode() {
         super.enterMode();
         mv.addMouseListener(this);
@@ -147,5 +147,6 @@
     }
 
-    @Override public void exitMode() {
+    @Override
+    public void exitMode() {
         super.exitMode();
         selectionManager.unregister(mv);
@@ -160,17 +161,22 @@
      * mouse (which will become selected).
      */
-    @Override public void mouseDragged(MouseEvent e) {
-        if(!mv.isActiveLayerVisible())
+    @Override
+    public void mouseDragged(MouseEvent e) {
+        if (!mv.isActiveLayerVisible())
             return;
 
         cancelDrawMode = true;
-        if (mode == Mode.select) return;
+        if (mode == Mode.select)
+            return;
 
         // do not count anything as a move if it lasts less than 100 milliseconds.
-        if ((mode == Mode.move) && (System.currentTimeMillis() - mouseDownTime < initialMoveDelay)) return;
-
-        if(mode != Mode.rotate) // button is pressed in rotate mode
+        if ((mode == Mode.move) && (System.currentTimeMillis() - mouseDownTime < initialMoveDelay))
+            return;
+
+        if (mode != Mode.rotate && mode != Mode.scale) // button is pressed in rotate mode
+        {
             if ((e.getModifiersEx() & MouseEvent.BUTTON1_DOWN_MASK) == 0)
                 return;
+        }
 
         if (mode == Mode.move) {
@@ -178,16 +184,22 @@
         }
 
+        if (startingDraggingPos == null) {
+            startingDraggingPos = new Point(e.getX(), e.getY());
+        }
+
         if (!initialMoveThresholdExceeded) {
-            int dxp = mousePos.x - e.getX();
-            int dyp = mousePos.y - e.getY();
-            int dp = (int) Math.sqrt(dxp*dxp+dyp*dyp);
-            if (dp < initialMoveThreshold) return;
+            int dxp = lastMousePos.x - e.getX();
+            int dyp = lastMousePos.y - e.getY();
+            int dp = (int) Math.sqrt(dxp * dxp + dyp * dyp);
+            if (dp < initialMoveThreshold)
+                return;
             initialMoveThresholdExceeded = true;
         }
 
-        EastNorth mouseEN = mv.getEastNorth(e.getX(), e.getY());
-        EastNorth mouseStartEN = mv.getEastNorth(mousePos.x, mousePos.y);
-        double dx = mouseEN.east() - mouseStartEN.east();
-        double dy = mouseEN.north() - mouseStartEN.north();
+        EastNorth currentEN = mv.getEastNorth(e.getX(), e.getY());
+        EastNorth lastEN = mv.getEastNorth(lastMousePos.x, lastMousePos.y);
+        //EastNorth startEN = mv.getEastNorth(startingDraggingPos.x, startingDraggingPos.y);
+        double dx = currentEN.east() - lastEN.east();
+        double dy = currentEN.north() - lastEN.north();
         if (dx == 0 && dy == 0)
             return;
@@ -199,5 +211,5 @@
                 Way w = virtualWay.way;
                 Way wnew = new Way(w);
-                wnew.addNode(virtualWay.lowerIndex+1, virtualNode);
+                wnew.addNode(virtualWay.lowerIndex + 1, virtualNode);
                 virtualCmds.add(new ChangeCommand(w, wnew));
             }
@@ -207,26 +219,26 @@
                     virtualWays.size());
             Main.main.undoRedo.add(new SequenceCommand(text, virtualCmds));
-            getCurrentDataSet().setSelected(Collections.singleton((OsmPrimitive)virtualNode));
+            getCurrentDataSet().setSelected(Collections.singleton((OsmPrimitive) virtualNode));
             virtualWays.clear();
             virtualNode = null;
         } else {
-            // Currently we support moving and rotating, which do not affect relations.
+            // Currently we support only transformations which do not affect relations.
             // So don't add them in the first place to make handling easier
             Collection<OsmPrimitive> selection = getCurrentDataSet().getSelectedNodesAndWays();
             Collection<Node> affectedNodes = AllNodesVisitor.getAllNodes(selection);
 
-            // when rotating, having only one node makes no sense - quit silently
-            if (mode == Mode.rotate && affectedNodes.size() < 2)
+            // for these transformations, having only one node makes no sense - quit silently
+            if (affectedNodes.size() < 2 && (mode == Mode.rotate || mode == Mode.scale))
                 return;
 
             Command c = !Main.main.undoRedo.commands.isEmpty()
-            ? Main.main.undoRedo.commands.getLast() : null;
+                    ? Main.main.undoRedo.commands.getLast() : null;
             if (c instanceof SequenceCommand) {
-                c = ((SequenceCommand)c).getLastCommand();
+                c = ((SequenceCommand) c).getLastCommand();
             }
 
             if (mode == Mode.move) {
-                if (c instanceof MoveCommand && affectedNodes.equals(((MoveCommand)c).getParticipatingPrimitives())) {
-                    ((MoveCommand)c).moveAgain(dx,dy);
+                if (c instanceof MoveCommand && affectedNodes.equals(((MoveCommand) c).getParticipatingPrimitives())) {
+                    ((MoveCommand) c).moveAgain(dx, dy);
                 } else {
                     Main.main.undoRedo.add(
@@ -243,7 +255,5 @@
                                 tr("Cannot move objects outside of the world."),
                                 tr("Warning"),
-                                JOptionPane.WARNING_MESSAGE
-
-                        );
+                                JOptionPane.WARNING_MESSAGE);
                         restoreCursor();
                         return;
@@ -251,8 +261,14 @@
                 }
             } else if (mode == Mode.rotate) {
-                if (c instanceof RotateCommand && affectedNodes.equals(((RotateCommand)c).getRotatedNodes())) {
-                    ((RotateCommand)c).rotateAgain(mouseStartEN, mouseEN);
+                if (c instanceof RotateCommand && affectedNodes.equals(((RotateCommand) c).getTransformedNodes())) {
+                    ((RotateCommand) c).handleEvent(currentEN);
                 } else {
-                    Main.main.undoRedo.add(new RotateCommand(selection, mouseStartEN, mouseEN));
+                    Main.main.undoRedo.add(new RotateCommand(selection, currentEN));
+                }
+            } else if (mode == Mode.scale) {
+                if (c instanceof ScaleCommand && affectedNodes.equals(((ScaleCommand) c).getTransformedNodes())) {
+                    ((ScaleCommand) c).handleEvent(currentEN);
+                } else {
+                    Main.main.undoRedo.add(new ScaleCommand(selection, currentEN));
                 }
             }
@@ -260,17 +276,19 @@
 
         mv.repaint();
-        mousePos = e.getPoint();
+        if (mode != Mode.scale) {
+            lastMousePos = e.getPoint();
+        }
 
         didMouseDrag = true;
     }
 
-    @Override public void mouseMoved(MouseEvent e) {
+    @Override
+    public void mouseMoved(MouseEvent e) {
         // Mac OSX simulates with  ctrl + mouse 1  the second mouse button hence no dragging events get fired.
         //
-        if ((Main.platform instanceof PlatformHookOsx) && mode == Mode.rotate) {
+        if ((Main.platform instanceof PlatformHookOsx) && (mode == Mode.rotate || mode == Mode.scale)) {
             mouseDragged(e);
         }
     }
-
     private Node virtualNode = null;
     private Collection<WaySegment> virtualWays = new LinkedList<WaySegment>();
@@ -295,14 +313,12 @@
             Way w = null;
 
-            for(WaySegment ws : mv.getNearestWaySegments(p, OsmPrimitive.isSelectablePredicate)) {
+            for (WaySegment ws : mv.getNearestWaySegments(p, OsmPrimitive.isSelectablePredicate)) {
                 w = ws.way;
 
                 Point2D p1 = mv.getPoint2D(wnp.a = w.getNode(ws.lowerIndex));
-                Point2D p2 = mv.getPoint2D(wnp.b = w.getNode(ws.lowerIndex+1));
-                if(SimplePaintVisitor.isLargeSegment(p1, p2, virtualSpace))
-                {
-                    Point2D pc = new Point2D.Double((p1.getX()+p2.getX())/2, (p1.getY()+p2.getY())/2);
-                    if (p.distanceSq(pc) < virtualSnapDistSq)
-                    {
+                Point2D p2 = mv.getPoint2D(wnp.b = w.getNode(ws.lowerIndex + 1));
+                if (SimplePaintVisitor.isLargeSegment(p1, p2, virtualSpace)) {
+                    Point2D pc = new Point2D.Double((p1.getX() + p2.getX()) / 2, (p1.getY() + p2.getY()) / 2);
+                    if (p.distanceSq(pc) < virtualSnapDistSq) {
                         // Check that only segments on top of each other get added to the
                         // virtual ways list. Otherwise ways that coincidentally have their
@@ -327,5 +343,4 @@
         return !virtualWays.isEmpty();
     }
-
     private Collection<OsmPrimitive> cycleList = Collections.emptyList();
     private boolean cyclePrims = false;
@@ -347,6 +362,5 @@
             boolean waitForMouseUp = Main.pref.getBoolean("mappaint.select.waits-for-mouse-up", false);
             boolean ctrl = (e.getModifiers() & ActionEvent.CTRL_MASK) != 0;
-            boolean alt = ((e.getModifiers() & (ActionEvent.ALT_MASK|InputEvent.ALT_GRAPH_MASK)) != 0
-                    || Main.pref.getBoolean("selectaction.cycles.multiple.matches", false));
+            boolean alt = ((e.getModifiers() & (ActionEvent.ALT_MASK | InputEvent.ALT_GRAPH_MASK)) != 0 || Main.pref.getBoolean("selectaction.cycles.multiple.matches", false));
 
             if (!alt) {
@@ -364,5 +378,5 @@
                 }
 
-                if (cycleList.size()>1) {
+                if (cycleList.size() > 1) {
                     cyclePrims = false;
 
@@ -378,5 +392,5 @@
                     // special case:  for cycle groups of 2, we can toggle to the
                     // true nearest primitive on mousePressed right away
-                    if (cycleList.size()==2 && !waitForMouseUp) {
+                    if (cycleList.size() == 2 && !waitForMouseUp) {
                         if (!(osm.equals(old) || osm.isNew() || ctrl)) {
                             cyclePrims = false;
@@ -405,12 +419,12 @@
      * cursor to movement.
      */
-    @Override public void mousePressed(MouseEvent e) {
+    @Override
+    public void mousePressed(MouseEvent e) {
         debug("mousePressed: e.getPoint()=" + e.getPoint());
 
         // return early
-        if(!mv.isActiveLayerVisible()
-                || !(Boolean)this.getValue("active")
-                || e.getButton() != MouseEvent.BUTTON1)
+        if (!mv.isActiveLayerVisible() || !(Boolean) this.getValue("active") || e.getButton() != MouseEvent.BUTTON1) {
             return;
+        }
 
         // request focus in order to enable the expected keyboard shortcuts
@@ -419,4 +433,5 @@
         boolean ctrl = (e.getModifiers() & ActionEvent.CTRL_MASK) != 0;
         boolean shift = (e.getModifiers() & ActionEvent.SHIFT_MASK) != 0;
+        boolean alt = (e.getModifiers() & ActionEvent.ALT_MASK) != 0;
 
         // We don't want to change to draw tool if the user tries to (de)select
@@ -426,5 +441,5 @@
         initialMoveThresholdExceeded = false;
         mouseDownTime = System.currentTimeMillis();
-        mousePos = e.getPoint();
+        lastMousePos = e.getPoint();
 
         Collection<OsmPrimitive> c = MapView.asColl(
@@ -443,4 +458,16 @@
             setCursor(ImageProvider.getCursor("rotate", null));
             mv.repaint();
+        } else if (alt && ctrl) {
+            mode = Mode.scale;
+
+            if (getCurrentDataSet().getSelected().isEmpty()) {
+                getCurrentDataSet().setSelected(c);
+            }
+
+            // Mode.select redraws when selectPrims is called
+            // Mode.move   redraws when mouseDragged is called
+            // Mode.scale redraws here
+            setCursor(ImageProvider.getCursor("scale", null));
+            mv.repaint();
         } else if (!c.isEmpty()) {
             mode = Mode.move;
@@ -466,6 +493,9 @@
         debug("mouseReleased: e.getPoint()=" + e.getPoint());
 
-        if(!mv.isActiveLayerVisible())
+        if (!mv.isActiveLayerVisible()) {
             return;
+        }
+
+        startingDraggingPos = null;
 
         restoreCursor();
@@ -474,5 +504,5 @@
 
             // Select Draw Tool if no selection has been made
-            if(getCurrentDataSet().getSelected().size() == 0 && !cancelDrawMode) {
+            if (getCurrentDataSet().getSelected().size() == 0 && !cancelDrawMode) {
                 Main.map.selectDrawTool(true);
                 return;
@@ -488,13 +518,14 @@
                 // do nothing if the click was to short to be recognized as a drag,
                 // but the release position is farther than 10px away from the press position
-                if (mousePos.distanceSq(e.getPoint())<100) {
+                if (lastMousePos.distanceSq(e.getPoint()) < 100) {
                     selectPrims(cyclePrims(cycleList, e), e, true, false);
 
                     // If the user double-clicked a node, change to draw mode
                     Collection<OsmPrimitive> c = getCurrentDataSet().getSelected();
-                    if(e.getClickCount() >=2 && c.size() == 1 && c.iterator().next() instanceof Node) {
+                    if (e.getClickCount() >= 2 && c.size() == 1 && c.iterator().next() instanceof Node) {
                         // We need to do it like this as otherwise drawAction will see a double
                         // click and switch back to SelectMode
-                        Main.worker.execute(new Runnable(){
+                        Main.worker.execute(new Runnable() {
+
                             public void run() {
                                 Main.map.selectDrawTool(true);
@@ -508,5 +539,5 @@
                 for (OsmPrimitive osm : getCurrentDataSet().getSelected()) {
                     if (osm instanceof Way) {
-                        limit -= ((Way)osm).getNodes().size();
+                        limit -= ((Way) osm).getNodes().size();
                     }
                     if ((limit -= 1) < 0) {
@@ -518,14 +549,12 @@
                             Main.parent,
                             tr("Move elements"),
-                            new String[] {tr("Move them"), tr("Undo move")});
-                    ed.setButtonIcons(new String[] {"reorder.png", "cancel.png"});
-                    ed.setContent(tr("You moved more than {0} elements. "
-                            + "Moving a large number of elements is often an error.\n"
-                            + "Really move them?", max));
+                            new String[]{tr("Move them"), tr("Undo move")});
+                    ed.setButtonIcons(new String[]{"reorder.png", "cancel.png"});
+                    ed.setContent(tr("You moved more than {0} elements. " + "Moving a large number of elements is often an error.\n" + "Really move them?", max));
                     ed.setCancelButton(2);
                     ed.toggleEnable("movedManyElements");
                     ed.showDialog();
 
-                    if(ed.getValue() != 1) {
+                    if (ed.getValue() != 1) {
                         Main.main.undoRedo.undo();
                     }
@@ -572,5 +601,5 @@
             nxt = first;
 
-            for (Iterator<OsmPrimitive> i = prims.iterator(); i.hasNext(); ) {
+            for (Iterator<OsmPrimitive> i = prims.iterator(); i.hasNext();) {
                 if (cyclePrims && shift) {
                     if (!(nxt = i.next()).isSelected()) {
@@ -631,5 +660,5 @@
 
                 Command cmd = MergeNodesAction.mergeNodes(Main.main.getEditLayer(), nodesToMerge, target.iterator().next());
-                if(cmd != null) {
+                if (cmd != null) {
                     Main.main.undoRedo.add(cmd);
                 }
@@ -644,6 +673,7 @@
 
         // not allowed together: do not change dataset selection, return early
-        if ((shift && ctrl) || (ctrl && !released) || (!virtualWays.isEmpty()))
+        if ((shift && ctrl) || (ctrl && !released) || (!virtualWays.isEmpty())) {
             return;
+        }
 
         if (!released) {
@@ -672,16 +702,21 @@
     }
 
-    @Override public String getModeHelpText() {
-        if (mode == Mode.select)
+    @Override
+    public String getModeHelpText() {
+        if (mode == Mode.select) {
             return tr("Release the mouse button to select the objects in the rectangle.");
-        else if (mode == Mode.move)
+        } else if (mode == Mode.move) {
             return tr("Release the mouse button to stop moving. Ctrl to merge with nearest node.");
-        else if (mode == Mode.rotate)
+        } else if (mode == Mode.rotate) {
             return tr("Release the mouse button to stop rotating.");
-        else
-            return tr("Move objects by dragging; Shift to add to selection (Ctrl to toggle); Shift-Ctrl to rotate selected; or change selection");
-    }
-
-    @Override public boolean layerIsSupported(Layer l) {
+        } else if (mode == Mode.scale) {
+            return tr("Release the mouse button to stop scaling.");
+        } else {
+            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");
+        }
+    }
+
+    @Override
+    public boolean layerIsSupported(Layer l) {
         return l instanceof OsmDataLayer;
     }
Index: trunk/src/org/openstreetmap/josm/command/RotateCommand.java
===================================================================
--- trunk/src/org/openstreetmap/josm/command/RotateCommand.java	(revision 3700)
+++ trunk/src/org/openstreetmap/josm/command/RotateCommand.java	(revision 3702)
@@ -1,2 +1,3 @@
+// License: GPL. For details, see LICENSE file.
 package org.openstreetmap.josm.command;
 
@@ -4,15 +5,10 @@
 
 import java.util.Collection;
-import java.util.HashMap;
-import java.util.LinkedList;
-import java.util.Map;
 
 import javax.swing.JLabel;
 
 import org.openstreetmap.josm.data.coor.EastNorth;
-import org.openstreetmap.josm.data.coor.LatLon;
 import org.openstreetmap.josm.data.osm.Node;
 import org.openstreetmap.josm.data.osm.OsmPrimitive;
-import org.openstreetmap.josm.data.osm.visitor.AllNodesVisitor;
 import org.openstreetmap.josm.tools.ImageProvider;
 
@@ -22,124 +18,87 @@
  * @author Frederik Ramm <frederik@remote.org>
  */
-public class RotateCommand extends Command {
+public class RotateCommand extends TransformNodesCommand {
 
     /**
-     * The objects to rotate.
-     */
-    private Collection<Node> nodes = new LinkedList<Node>();
-
-    /**
-     * pivot point
+     * Pivot point
      */
     private EastNorth pivot;
 
     /**
-     * Small helper for holding the interesting part of the old data state of the
-     * objects.
+     * World position of the mouse when the user started the command.
+     *
      */
-    public static class OldState {
-        LatLon latlon;
-        EastNorth eastNorth;
-        boolean modified;
-    }
+    EastNorth startEN = null;
 
     /**
      * angle of rotation starting click to pivot
      */
-    private double startAngle;
+    private double startAngle = 0.0;
 
     /**
      * computed rotation angle between starting click and current mouse pos
      */
-    private double rotationAngle;
-
-    /**
-     * List of all old states of the objects.
-     */
-    private Map<Node, OldState> oldState = new HashMap<Node, OldState>();
+    private double rotationAngle = 0.0;
 
     /**
      * Creates a RotateCommand.
-     * Assign the initial object set, compute pivot point and rotation angle.
-     * Computation of pivot point is done by the same rules that are used in
-     * the "align nodes in circle" action.
+     * Assign the initial object set, compute pivot point and inital rotation angle.
      */
-    public RotateCommand(Collection<OsmPrimitive> objects, EastNorth start, EastNorth end) {
+    public RotateCommand(Collection<OsmPrimitive> objects, EastNorth currentEN) {
+        super(objects);
 
-        this.nodes = AllNodesVisitor.getAllNodes(objects);
-        pivot = new EastNorth(0,0);
+        pivot = getNodesCenter();
 
-        for (Node n : this.nodes) {
-            OldState os = new OldState();
-            os.latlon = new LatLon(n.getCoor());
-            os.eastNorth = n.getEastNorth();
-            os.modified = n.isModified();
-            oldState.put(n, os);
-            pivot = pivot.add(os.eastNorth.east(), os.eastNorth.north());
-        }
-        pivot = new EastNorth(pivot.east()/this.nodes.size(), pivot.north()/this.nodes.size());
+        // We remember the very first position of the mouse for this action.
+        // Note that SelectAction will keep the same ScaleCommand when the user
+        // releases the button and presses it again with the same modifiers.
+        // The very first point of this operation is stored here.
+        startEN   = currentEN;
 
-        rotationAngle = Math.PI/2;
-        rotateAgain(start, end);
+        startAngle = getAngle(currentEN);
+        rotationAngle = 0.0;
+
+        handleEvent(currentEN);
+    }
+    
+    /**
+     * Get angle between the horizontal axis and the line formed by the pivot and give points.
+     **/
+    protected double getAngle(EastNorth currentEN) {
+        if ( pivot == null )
+            return 0.0; // should never happen by contract
+        return Math.atan2(currentEN.east()-pivot.east(), currentEN.north()-pivot.north());
     }
 
     /**
-     * Rotate the same set of objects again, by the angle between given
-     * start and end nodes. Internally this is added to the existing
-     * rotation so a later undo will undo the whole rotation.
+     * Compute new rotation angle and transform nodes accordingly.
      */
-    public void rotateAgain(EastNorth start, EastNorth end) {
-        // compute angle
-        startAngle = Math.atan2(start.east()-pivot.east(), start.north()-pivot.north());
-        double endAngle = Math.atan2(end.east()-pivot.east(), end.north()-pivot.north());
-        rotationAngle += startAngle - endAngle;
-        rotateNodes(false);
+    @Override
+    public void handleEvent(EastNorth currentEN) {
+        double currentAngle = getAngle(currentEN);
+        rotationAngle = currentAngle - startAngle;
+        transformNodes();
     }
 
     /**
-     * Helper for actually rotationg the nodes.
-     * @param setModified - true if rotated nodes should be flagged "modified"
+     * Rotate nodes.
      */
-    private void rotateNodes(boolean setModified) {
+    @Override
+    protected void transformNodes() {
         for (Node n : nodes) {
             double cosPhi = Math.cos(rotationAngle);
             double sinPhi = Math.sin(rotationAngle);
-            EastNorth oldEastNorth = oldState.get(n).eastNorth;
+            EastNorth oldEastNorth = oldStates.get(n).eastNorth;
             double x = oldEastNorth.east() - pivot.east();
             double y = oldEastNorth.north() - pivot.north();
-            double nx =  sinPhi * x + cosPhi * y + pivot.east();
-            double ny = -cosPhi * x + sinPhi * y + pivot.north();
+            double nx =  cosPhi * x + sinPhi * y + pivot.east();
+            double ny = -sinPhi * x + cosPhi * y + pivot.north();
             n.setEastNorth(new EastNorth(nx, ny));
-            if (setModified) {
-                n.setModified(true);
-            }
         }
     }
 
-    @Override public boolean executeCommand() {
-        rotateNodes(true);
-        return true;
-    }
-
-    @Override public void undoCommand() {
-        for (Node n : nodes) {
-            OldState os = oldState.get(n);
-            n.setCoor(os.latlon);
-            n.setModified(os.modified);
-        }
-    }
-
-    @Override public void fillModifiedData(Collection<OsmPrimitive> modified, Collection<OsmPrimitive> deleted, Collection<OsmPrimitive> added) {
-        for (OsmPrimitive osm : nodes) {
-            modified.add(osm);
-        }
-    }
-
-    @Override public JLabel getDescription() {
+    @Override
+    public JLabel getDescription() {
         return new JLabel(trn("Rotate {0} node", "Rotate {0} nodes", nodes.size(), nodes.size()), ImageProvider.get("data", "node"), JLabel.HORIZONTAL);
     }
-
-    public Collection<Node> getRotatedNodes() {
-        return nodes;
-    }
 }
Index: trunk/src/org/openstreetmap/josm/command/ScaleCommand.java
===================================================================
--- trunk/src/org/openstreetmap/josm/command/ScaleCommand.java	(revision 3702)
+++ trunk/src/org/openstreetmap/josm/command/ScaleCommand.java	(revision 3702)
@@ -0,0 +1,87 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.command;
+
+import static org.openstreetmap.josm.tools.I18n.trn;
+
+import java.util.Collection;
+
+import javax.swing.JLabel;
+
+import org.openstreetmap.josm.data.coor.EastNorth;
+import org.openstreetmap.josm.data.osm.Node;
+import org.openstreetmap.josm.data.osm.OsmPrimitive;
+import org.openstreetmap.josm.tools.ImageProvider;
+
+public class ScaleCommand extends TransformNodesCommand {
+    /**
+     * Pivot point
+     */
+    private EastNorth pivot;
+
+    /**
+     * Current scaling factor applied
+     */
+    private double scalingFactor;
+
+    /**
+     * World position of the mouse when the user started the command.
+     *
+     */
+    EastNorth startEN = null;
+
+    /**
+     * Creates a ScaleCommand.
+     * Assign the initial object set, compute pivot point.
+     * Computation of pivot point is done by the same rules that are used in
+     * the "align nodes in circle" action.
+     */
+    public ScaleCommand(Collection<OsmPrimitive> objects, EastNorth currentEN) {
+        super(objects);
+
+        pivot = getNodesCenter();
+
+        // We remember the very first position of the mouse for this action.
+        // Note that SelectAction will keep the same ScaleCommand when the user
+        // releases the button and presses it again with the same modifiers.
+        // The very first point of this operation is stored here.
+        startEN   = currentEN;
+
+        handleEvent(currentEN);
+    }
+
+    /**
+     * Compute new scaling factor and transform nodes accordingly.
+     */
+    @Override
+    public void handleEvent(EastNorth currentEN) {
+        double startAngle = Math.atan2(startEN.east()-pivot.east(), startEN.north()-pivot.north());
+        double endAngle = Math.atan2(currentEN.east()-pivot.east(), currentEN.north()-pivot.north());
+        double startDistance = pivot.distance(startEN);
+        double currentDistance = pivot.distance(currentEN);
+        scalingFactor = Math.cos(startAngle-endAngle) * currentDistance / startDistance;
+        transformNodes();
+    }
+
+
+    /**
+     * Scale nodes.
+     */
+    @Override
+    protected void transformNodes() {
+        // scalingFactor = 2.0;
+        for (Node n : nodes) {
+            EastNorth oldEastNorth = oldStates.get(n).eastNorth;
+            double dx = oldEastNorth.east() - pivot.east();
+            double dy = oldEastNorth.north() - pivot.north();
+            double nx = pivot.east() + scalingFactor * dx;
+            double ny = pivot.north() + scalingFactor * dy;
+            n.setEastNorth(new EastNorth(nx, ny));
+        }
+    }
+
+    @Override
+    public JLabel getDescription() {
+        return new JLabel(trn("Scale {0} node", "Scale {0} nodes", nodes.size(), nodes.size()), ImageProvider.get("data", "node"), JLabel.HORIZONTAL);
+    }
+
+}
Index: trunk/src/org/openstreetmap/josm/command/TransformNodesCommand.java
===================================================================
--- trunk/src/org/openstreetmap/josm/command/TransformNodesCommand.java	(revision 3702)
+++ trunk/src/org/openstreetmap/josm/command/TransformNodesCommand.java	(revision 3702)
@@ -0,0 +1,150 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.command;
+
+import static org.openstreetmap.josm.tools.I18n.trn;
+
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.Map;
+
+import javax.swing.JLabel;
+
+import org.openstreetmap.josm.data.coor.EastNorth;
+import org.openstreetmap.josm.data.coor.LatLon;
+import org.openstreetmap.josm.data.osm.Node;
+import org.openstreetmap.josm.data.osm.OsmPrimitive;
+import org.openstreetmap.josm.data.osm.visitor.AllNodesVisitor;
+import org.openstreetmap.josm.tools.ImageProvider;
+
+/**
+ * Abstract class with common services for nodes rotation and scaling commands.
+ *
+ * @author Olivier Croquette <ocroquette@free.fr>
+ */
+public abstract class TransformNodesCommand extends Command {
+
+    /**
+     * The nodes to transform.
+     */
+    protected Collection<Node> nodes = new LinkedList<Node>();
+
+    /**
+     * Small helper for holding the interesting part of the old data state of the
+     * nodes.
+     */
+    public static class OldState {
+        LatLon latlon;
+        EastNorth eastNorth;
+        boolean modified;
+    }
+
+    /**
+     * List of all old states of the nodes.
+     */
+    protected Map<Node, OldState> oldStates = new HashMap<Node, OldState>();
+
+    /**
+     * Stores the state of the nodes before the command.
+     */
+    protected void storeOldState() {
+        for (Node n : this.nodes) {
+            OldState os = new OldState();
+            os.latlon = new LatLon(n.getCoor());
+            os.eastNorth = n.getEastNorth();
+            os.modified = n.isModified();
+            oldStates.put(n, os);
+        }
+    }
+
+    /**
+     * Creates a TransformNodesObject.
+     * Find out the impacted nodes and store their initial state.
+     */
+    public TransformNodesCommand(Collection<OsmPrimitive> objects) {
+        this.nodes = AllNodesVisitor.getAllNodes(objects);
+        storeOldState();
+    }
+
+    /**
+     * Handling of a mouse event (e.g. dragging event).
+     * @param currentEN the current world position of the mouse
+     */
+    public abstract void handleEvent(EastNorth currentEN);
+
+    /**
+     * Implementation for the nodes transformation.
+     * No parameters are given here, you should handle the user input in handleEvent()
+     * and store it internally.
+     */
+    protected abstract void transformNodes();
+
+    /**
+     * Finally apply the transformation of the nodes.
+     * This is called when the user is happy with the current state of the command
+     * and its effects.
+     */
+    @Override
+    public boolean executeCommand() {
+        transformNodes();
+        flagNodesAsModified();
+        return true;
+    }
+
+    /**
+     * Flag all nodes as modified.
+     */
+    public void flagNodesAsModified() {
+        for (Node n : nodes) {
+            n.setModified(true);
+        }
+    }
+
+    /**
+     * Restore the state of the nodes from the backup.
+     */
+    @Override
+    public void undoCommand() {
+        for (Node n : nodes) {
+            OldState os = oldStates.get(n);
+            n.setCoor(os.latlon);
+            n.setModified(os.modified);
+        }
+    }
+
+    @Override
+    public void fillModifiedData(Collection<OsmPrimitive> modified, Collection<OsmPrimitive> deleted, Collection<OsmPrimitive> added) {
+    }
+
+    @Override
+    public Collection<? extends OsmPrimitive> getParticipatingPrimitives() {
+        return nodes;
+    }
+
+    @Override
+    public JLabel getDescription() {
+        return new JLabel(trn("Transform {0} node", "Transform {0} nodes", nodes.size(), nodes.size()), ImageProvider.get("data", "node"), JLabel.HORIZONTAL);
+    }
+
+    /**
+     * Get the nodes with the current transformation applied.
+     */
+    public Collection<Node> getTransformedNodes() {
+        return nodes;
+    }
+
+    /**
+     * Get the center of the nodes under modification.
+     * It's just the barycenter.
+     */
+    public EastNorth getNodesCenter() {
+        EastNorth sum = new EastNorth(0,0);
+
+        for (Node n : nodes ) {
+            EastNorth en = n.getEastNorth();
+            sum = sum.add(en.east(), en.north());
+        }
+        return new EastNorth(sum.east()/this.nodes.size(), sum.north()/this.nodes.size());
+
+    }
+}
