Ticket #3832: josm-2374-extrude-movenormal-ris-v3.diff

File josm-2374-extrude-movenormal-ris-v3.diff, 22.1 KB (added by ris, 14 years ago)
  • src/org/openstreetmap/josm/actions/mapmode/ExtrudeAction.java

     
    1010import java.awt.Graphics;
    1111import java.awt.Graphics2D;
    1212import java.awt.Point;
     13import java.awt.Rectangle;
    1314import java.awt.event.KeyEvent;
    1415import java.awt.event.MouseEvent;
     16import java.awt.event.ActionEvent;
     17import java.awt.geom.AffineTransform;
     18import java.awt.geom.NoninvertibleTransformException;
    1519import java.awt.geom.GeneralPath;
     20import java.awt.geom.Point2D;
     21import java.awt.geom.Line2D;
     22import java.awt.geom.Line2D.Double;
     23import java.lang.Math;
    1624import java.util.Collection;
    1725import java.util.LinkedList;
    1826
     
    2028import org.openstreetmap.josm.command.AddCommand;
    2129import org.openstreetmap.josm.command.ChangeCommand;
    2230import org.openstreetmap.josm.command.Command;
     31import org.openstreetmap.josm.command.MoveCommand;
    2332import org.openstreetmap.josm.command.SequenceCommand;
    2433import org.openstreetmap.josm.data.coor.EastNorth;
    2534import org.openstreetmap.josm.data.osm.Node;
     35import org.openstreetmap.josm.data.osm.OsmPrimitive;
    2636import org.openstreetmap.josm.data.osm.Way;
    2737import org.openstreetmap.josm.data.osm.WaySegment;
    2838import org.openstreetmap.josm.gui.MapFrame;
     
    3545
    3646/**
    3747 * Makes a rectangle from a line, or modifies a rectangle.
    38  *
    39  * This class currently contains some "sleeping" code copied from DrawAction (move and rotate)
    40  * which can eventually be removed, but it may also get activated here and removed in DrawAction.
    4148 */
    4249public class ExtrudeAction extends MapMode implements MapViewPaintable {
    4350
    44     enum Mode { EXTRUDE, rotate, select }
    45     private Mode mode = null;
     51    enum Mode { extrude, translate, select }
     52    private Mode mode = Mode.select;
    4653    private long mouseDownTime = 0;
    4754    private WaySegment selectedSegment = null;
    4855    private Color selectedColor;
    4956
    50     double xoff;
    51     double yoff;
    52     double distance;
    53 
    5457    /**
    5558     * The old cursor before the user pressed the mouse button.
    5659     */
    5760    private Cursor oldCursor;
    5861    /**
    59      * The current position of the mouse
    60      */
    61     private Point mousePos;
    62     /**
    6362     * The position of the mouse cursor when the drag action was initiated.
    6463     */
    6564    private Point initialMousePos;
     
    6766     * The time which needs to pass between click and release before something
    6867     * counts as a move, in milliseconds
    6968     */
    70     private int initialMoveDelay = 200;
     69    private static int initialMoveDelay = 200;
     70    /**
     71     * The initial EastNorths of node1 and node2
     72     */
     73    private EastNorth initialN1en;
     74    private EastNorth initialN2en;
     75    /**
     76     * The new EastNorths of node1 and node2
     77     */
     78    private EastNorth newN1en;
     79    private EastNorth newN2en;
     80    /**
     81     * This is to work around some deficiencies in MoveCommand when translating
     82     */
     83    private EastNorth lastTranslatedN1en;
     84    /**
     85     * Normal unit vector of the selected segment.
     86     */
     87    private EastNorth normalUnitVector;
     88    /**
     89     * Vector of node2 from node1.
     90     */
     91    private EastNorth segmentVector;
     92    /**
     93     * Transforms the mouse point (in EastNorth space) to the normal-shifted position
     94     * of point 1 of the selectedSegment.
     95     */
     96    private AffineTransform normalTransform;
    7197
    7298    /**
    7399     * Create a new SelectAction
     
    116142        Main.map.mapView.removeMouseListener(this);
    117143        Main.map.mapView.removeMouseMotionListener(this);
    118144        Main.map.mapView.removeTemporaryLayer(this);
    119 
    120145    }
    121146
    122147    /**
    123      * If the left mouse button is pressed, move all currently selected
    124      * objects (if one of them is under the mouse) or the current one under the
    125      * mouse (which will become selected).
     148     * Perform action depending on what mode we're in.
    126149     */
    127150    @Override public void mouseDragged(MouseEvent e) {
    128151        if(!Main.map.mapView.isActiveLayerVisible())
    129152            return;
    130         if (mode == Mode.select) return;
    131153
    132         // do not count anything as a move if it lasts less than 100 milliseconds.
    133         if ((mode == Mode.EXTRUDE) && (System.currentTimeMillis() - mouseDownTime < initialMoveDelay)) return;
     154        // do not count anything as a drag if it lasts less than 100 milliseconds.
     155        if (System.currentTimeMillis() - mouseDownTime < initialMoveDelay) return;
    134156
    135         if ((e.getModifiersEx() & MouseEvent.BUTTON1_DOWN_MASK) == 0)
    136             return;
     157        if (mode == Mode.select) {
     158            // Just sit tight and wait for mouse to be released.
     159        } else {
     160            // This may be ugly, but I can't see any other way of getting a mapview from here.
     161            EastNorth mouseen = Main.map.mapView.getEastNorth(e.getPoint().x, e.getPoint().y);
    137162
    138         if (mode == Mode.EXTRUDE) {
     163            Point2D newN1point = normalTransform.transform(mouseen, null);
     164
     165            newN1en = new EastNorth(newN1point.getX(), newN1point.getY());
     166            newN2en = newN1en.add(segmentVector.getX(), segmentVector.getY());
     167
     168            // find out the distance, in metres, between the initial position of N1 and the new one.
     169            Main.map.statusLine.setDist(Main.proj.eastNorth2latlon(initialN1en).greatCircleDistance(Main.proj.eastNorth2latlon(newN1en)));
     170            updateStatusLine();
     171
    139172            setCursor(Cursor.getPredefinedCursor(Cursor.MOVE_CURSOR));
    140         }
    141173
    142         if (mousePos == null) {
    143             mousePos = e.getPoint();
    144             return;
     174            if (mode == Mode.extrude) {
     175
     176            } else if (mode == Mode.translate) {
     177                Command c = !Main.main.undoRedo.commands.isEmpty()
     178                ? Main.main.undoRedo.commands.getLast() : null;
     179                if (c instanceof SequenceCommand) {
     180                    c = ((SequenceCommand)c).getLastCommand();
     181                }
     182
     183                Node n1 = selectedSegment.way.getNode(selectedSegment.lowerIndex);
     184                Node n2 = selectedSegment.way.getNode(selectedSegment.lowerIndex+1);
     185
     186                EastNorth difference = new EastNorth(newN1en.getX()-lastTranslatedN1en.getX(), newN1en.getY()-lastTranslatedN1en.getY());
     187
     188                // Better way of testing list equality non-order-sensitively?
     189                if (c instanceof MoveCommand
     190                && ((MoveCommand)c).getMovedNodes().contains(n1)
     191                && ((MoveCommand)c).getMovedNodes().contains(n2)
     192                && ((MoveCommand)c).getMovedNodes().size() == 2) {
     193                    // MoveCommand doesn't let us know how much it has already moved the selection
     194                    // so we have to do some ugly record-keeping.
     195                    ((MoveCommand)c).moveAgain(difference.getX(), difference.getY());
     196                    lastTranslatedN1en = newN1en;
     197                } else {
     198                    Collection<OsmPrimitive> nodelist = new LinkedList<OsmPrimitive>();
     199                    nodelist.add(n1);
     200                    nodelist.add(n2);
     201                    Main.main.undoRedo.add(c = new MoveCommand(nodelist, difference.getX(), difference.getY()));
     202                    lastTranslatedN1en = newN1en;
     203                }
     204            }
     205            Main.map.mapView.repaint();
    145206        }
     207    }
    146208
    147         Main.map.mapView.repaint();
    148         mousePos = e.getPoint();
     209    /**
     210     * Create a new Line that extends off the edge of the viewport in one direction
     211     * @param start The start point of the line
     212     * @param unitvector A unit vector denoting the direction of the line
     213     * @param g the Graphics2D object  it will be used on
     214     */
     215    static private Line2D createSemiInfiniteLine(Point2D start, Point2D unitvector, Graphics2D g) {
     216        Rectangle bounds = g.getDeviceConfiguration().getBounds();
     217        try {
     218            AffineTransform invtrans = g.getTransform().createInverse();
     219            Point2D widthpoint = invtrans.deltaTransform(new Point2D.Double(bounds.width,0), null);
     220            Point2D heightpoint = invtrans.deltaTransform(new Point2D.Double(0,bounds.height), null);
    149221
     222            // Here we should end up with a gross overestimate of the maximum viewport diagonal in what
     223            // Graphics2D calls 'user space'. Essentially a manhattan distance of manhattan distances.
     224            // This can be used as a safe length of line to generate which will always go off-viewport.
     225            double linelength = Math.abs(widthpoint.getX()) + Math.abs(widthpoint.getY()) + Math.abs(heightpoint.getX()) + Math.abs(heightpoint.getY());
     226
     227            return new Line2D.Double(start, new Point2D.Double(start.getX() + (unitvector.getX() * linelength) , start.getY() + (unitvector.getY() * linelength)));
     228        }
     229        catch (NoninvertibleTransformException e) {
     230            return new Line2D.Double(start, new Point2D.Double(start.getX() + (unitvector.getX() * 10) , start.getY() + (unitvector.getY() * 10)));
     231        }
    150232    }
    151233
    152234    public void paint(Graphics g, MapView mv) {
    153         if (selectedSegment != null) {
    154             Node n1 = selectedSegment.way.getNode(selectedSegment.lowerIndex);
    155             Node n2 = selectedSegment.way.getNode(selectedSegment.lowerIndex + 1);
     235        if (mode == Mode.select) {
     236            // Nothing to do
     237        } else {
     238            if (newN1en != null) {
     239                Graphics2D g2 = (Graphics2D)g;
     240                g2.setColor(selectedColor);
     241                g2.setStroke(new BasicStroke(3, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND));
    156242
    157             EastNorth en1 = n1.getEastNorth();
    158             EastNorth en2 = n2.getEastNorth();
    159             EastNorth en3 = mv.getEastNorth(mousePos.x, mousePos.y);
     243                Point p1 = mv.getPoint(initialN1en);
     244                Point p2 = mv.getPoint(initialN2en);
     245                Point p3 = mv.getPoint(newN1en);
     246                Point p4 = mv.getPoint(newN2en);
    160247
    161             double u = ((en3.east() - en1.east()) * (en2.east() - en1.east()) +
    162                     (en3.north() - en1.north()) * (en2.north() - en1.north())) /
    163                     en2.distanceSq(en1);
    164             // the point on the segment from which the distance to mouse pos is shortest
    165             EastNorth base = new EastNorth(en1.east() + u * (en2.east() - en1.east()),
    166                     en1.north() + u * (en2.north() - en1.north()));
     248                if (mode == Mode.extrude) {
     249                    // Draw rectangle around new area.
     250                    GeneralPath b = new GeneralPath();
     251                    b.moveTo(p1.x, p1.y); b.lineTo(p3.x, p3.y);
     252                    b.lineTo(p4.x, p4.y); b.lineTo(p2.x, p2.y);
     253                    b.lineTo(p1.x, p1.y);
     254                    g2.draw(b);
     255                    g2.setStroke(new BasicStroke(1));
     256                } else if (mode == Mode.translate) {
     257                    // Highlight the new and old segments.
     258                    Line2D newline = new Line2D.Double(p3, p4);
     259                    g2.draw(newline);
     260                    g2.setStroke(new BasicStroke(1));
     261                    Line2D oldline = new Line2D.Double(p1, p2);
     262                    g2.draw(oldline);
    167263
    168             // find out the distance, in metres, between the base point and the mouse cursor
    169             distance = Main.proj.eastNorth2latlon(base).greatCircleDistance(Main.proj.eastNorth2latlon(en3));
    170             Main.map.statusLine.setDist(distance);
    171             updateStatusLine();
     264                    // Draw a guideline along the normal.
     265                    Line2D normline;
     266                    Point2D centerpoint = new Point2D.Double((p1.getX()+p2.getX())*0.5, (p1.getY()+p2.getY())*0.5);
     267                    EastNorth drawnorm;
     268                    // Check to see if our new N1 is in a positive direction with respect to the normalUnitVector.
     269                    // Even if the x component is zero, we should still be able to discern using +0.0 and -0.0
     270                    if (newN1en == null || (newN1en.getX() > initialN1en.getX() == normalUnitVector.getX() > -0.0))
     271                        drawnorm = normalUnitVector;
     272                    else
     273                        // If not, use a sign-flipped version of the normalUnitVector.
     274                        drawnorm = new EastNorth(-normalUnitVector.getX(), -normalUnitVector.getY());
     275                    normline = createSemiInfiniteLine(centerpoint, drawnorm, g2);
     276                    g2.draw(normline);
    172277
    173             // compute vertical and horizontal components.
    174             xoff = en3.east() - base.east();
    175             yoff = en3.north() - base.north();
     278                    // EastNorth units per pixel
     279                    double factor = 1.0/g2.getTransform().getScaleX();
    176280
    177             Graphics2D g2 = (Graphics2D)g;
    178             g2.setColor(selectedColor);
    179             g2.setStroke(new BasicStroke(3, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND));
    180             GeneralPath b = new GeneralPath();
    181             Point p1 = mv.getPoint(en1);
    182             Point p2 = mv.getPoint(en2);
    183             Point p3 = mv.getPoint(en1.add(xoff, yoff));
    184             Point p4 = mv.getPoint(en2.add(xoff, yoff));
    185 
    186             b.moveTo(p1.x, p1.y); b.lineTo(p3.x, p3.y);
    187             b.lineTo(p4.x, p4.y); b.lineTo(p2.x, p2.y);
    188             b.lineTo(p1.x, p1.y);
    189             g2.draw(b);
    190             g2.setStroke(new BasicStroke(1));
     281                    // Draw right angle marker on initial position.
     282                    double raoffsetx = 8.0*factor*drawnorm.getX();
     283                    double raoffsety = 8.0*factor*drawnorm.getY();
     284                    Point2D ra1 = new Point2D.Double(centerpoint.getX()+raoffsetx, centerpoint.getY()+raoffsety);
     285                    Point2D ra3 = new Point2D.Double(centerpoint.getX()-raoffsety, centerpoint.getY()+raoffsetx);
     286                    Point2D ra2 = new Point2D.Double(ra1.getX()-raoffsety, ra1.getY()+raoffsetx);
     287                    GeneralPath ra = new GeneralPath();
     288                    ra.moveTo(ra1.getX(), ra1.getY());
     289                    ra.lineTo(ra2.getX(), ra2.getY());
     290                    ra.lineTo(ra3.getX(), ra3.getY());
     291                    g2.draw(ra);
     292                }
     293            }
    191294        }
    192295    }
    193296
    194297    /**
     298     * If the left mouse button is pressed over a segment, switch
     299     * to either extrude or translate mode depending on whether ctrl is held.
    195300     */
    196301    @Override public void mousePressed(MouseEvent e) {
    197302        if(!Main.map.mapView.isActiveLayerVisible())
     
    203308        // boolean alt = (e.getModifiers() & ActionEvent.ALT_MASK) != 0;
    204309        // boolean shift = (e.getModifiers() & ActionEvent.SHIFT_MASK) != 0;
    205310
    206         mouseDownTime = System.currentTimeMillis();
     311        selectedSegment = Main.map.mapView.getNearestWaySegment(e.getPoint());
    207312
    208         selectedSegment =
    209             Main.map.mapView.getNearestWaySegment(e.getPoint());
     313        if (selectedSegment == null) {
     314            // If nothing gets caught, stay in select mode
     315        } else {
     316            // Otherwise switch to another mode
    210317
    211         mode = (selectedSegment == null) ? Mode.select : Mode.EXTRUDE;
    212         oldCursor = Main.map.mapView.getCursor();
     318            // For extrusion, these positions are actually never changed,
     319            // but keeping note of this anyway allows us to not continually
     320            // look it up and also allows us to unify code with the translate mode
     321            initialN1en = selectedSegment.way.getNode(selectedSegment.lowerIndex).getEastNorth();
     322            initialN2en = selectedSegment.way.getNode(selectedSegment.lowerIndex + 1).getEastNorth();
    213323
    214         updateStatusLine();
    215         Main.map.mapView.addTemporaryLayer(this);
    216         Main.map.mapView.repaint();
     324            // Signifies that nothing has happened yet
     325            newN1en = null;
     326            newN2en = null;
    217327
    218         mousePos = e.getPoint();
    219         initialMousePos = e.getPoint();
     328            Main.map.mapView.addTemporaryLayer(this);
    220329
    221         if(selectedSegment != null) {
    222             getCurrentDataSet().setSelected(selectedSegment.way);
     330            updateStatusLine();
     331            Main.map.mapView.repaint();
     332
     333            // Make note of time pressed
     334            mouseDownTime = System.currentTimeMillis();
     335
     336            // Make note of mouse position
     337            initialMousePos = e.getPoint();
     338
     339            segmentVector = new EastNorth(initialN2en.getX()-initialN1en.getX(), initialN2en.getY()-initialN1en.getY());
     340            double factor = 1.0 / Math.hypot(segmentVector.getX(), segmentVector.getY());
     341            // swap coords to get normal, mult by factor to get unit vector.
     342            normalUnitVector = new EastNorth(segmentVector.getY() * factor, segmentVector.getX() * factor);
     343
     344            // The calculation of points along the normal of the segment from mouse
     345            // points is actually a purely affine mapping. So the majority of the maths
     346            // can be done once, on mousePress, by building an AffineTransform which
     347            // we can use in the other functions.
     348            double r = 1.0 / ( (normalUnitVector.getX()*normalUnitVector.getX()) + (normalUnitVector.getY()*normalUnitVector.getY()) );
     349            double s = (normalUnitVector.getX()*initialN1en.getX()) - (normalUnitVector.getY()*initialN1en.getY());
     350            double compcoordcoeff = -r*normalUnitVector.getX()*normalUnitVector.getY();
     351
     352            // Build the matrix. Takes a mouse position in EastNorth-space and returns the new position of node1
     353            // based on that.
     354            normalTransform = new AffineTransform(
     355                r*normalUnitVector.getX()*normalUnitVector.getX(), compcoordcoeff,
     356                compcoordcoeff, r*normalUnitVector.getY()*normalUnitVector.getY(),
     357                initialN1en.getX()-(s*r*normalUnitVector.getX()), initialN1en.getY()+(s*r*normalUnitVector.getY()));
     358
     359            // Switch mode.
     360            if ( (e.getModifiers() & ActionEvent.CTRL_MASK) != 0 ) {
     361                mode = Mode.translate;
     362                lastTranslatedN1en = initialN1en;
     363            } else {
     364                mode = Mode.extrude;
     365                getCurrentDataSet().setSelected(selectedSegment.way);
     366            }
    223367        }
    224368    }
    225369
    226370    /**
    227      * Restore the old mouse cursor.
     371     * Do anything that needs to be done, then switch back to select mode
    228372     */
    229373    @Override public void mouseReleased(MouseEvent e) {
     374
    230375        if(!Main.map.mapView.isActiveLayerVisible())
    231376            return;
    232         restoreCursor();
    233         if (selectedSegment == null) return;
    234         if (mousePos.distance(initialMousePos) > 10) {
    235             Node n1 = selectedSegment.way.getNode(selectedSegment.lowerIndex);
    236             Node n2 = selectedSegment.way.getNode(selectedSegment.lowerIndex+1);
    237             EastNorth en3 = n2.getEastNorth().add(xoff, yoff);
    238             Node n3 = new Node(Main.proj.eastNorth2latlon(en3));
    239             EastNorth en4 = n1.getEastNorth().add(xoff, yoff);
    240             Node n4 = new Node(Main.proj.eastNorth2latlon(en4));
    241             Way wnew = new Way(selectedSegment.way);
    242             wnew.addNode(selectedSegment.lowerIndex+1, n3);
    243             wnew.addNode(selectedSegment.lowerIndex+1, n4);
    244             if (wnew.getNodesCount() == 4) {
    245                 wnew.addNode(n1);
     377
     378        if (mode == mode.select) {
     379            // Nothing to be done
     380        } else {
     381            if (mode == mode.extrude) {
     382                if (e.getPoint().distance(initialMousePos) > 10 && newN1en != null) {
     383                    // Commit extrusion
     384
     385                    Node n1 = selectedSegment.way.getNode(selectedSegment.lowerIndex);
     386                    Node n2 = selectedSegment.way.getNode(selectedSegment.lowerIndex+1);
     387                    Node n3 = new Node(Main.proj.eastNorth2latlon(newN2en));
     388                    Node n4 = new Node(Main.proj.eastNorth2latlon(newN1en));
     389                    Way wnew = new Way(selectedSegment.way);
     390                    wnew.addNode(selectedSegment.lowerIndex+1, n3);
     391                    wnew.addNode(selectedSegment.lowerIndex+1, n4);
     392                    if (wnew.getNodesCount() == 4) {
     393                        wnew.addNode(n1);
     394                    }
     395                    Collection<Command> cmds = new LinkedList<Command>();
     396                    cmds.add(new AddCommand(n4));
     397                    cmds.add(new AddCommand(n3));
     398                    cmds.add(new ChangeCommand(selectedSegment.way, wnew));
     399                    Command c = new SequenceCommand(tr("Extrude Way"), cmds);
     400                    Main.main.undoRedo.add(c);
     401                }
     402            } else if (mode == mode.translate) {
     403                // I don't think there's anything to do
    246404            }
    247             Collection<Command> cmds = new LinkedList<Command>();
    248             cmds.add(new AddCommand(n4));
    249             cmds.add(new AddCommand(n3));
    250             cmds.add(new ChangeCommand(selectedSegment.way, wnew));
    251             Command c = new SequenceCommand(tr("Extrude Way"), cmds);
    252             Main.main.undoRedo.add(c);
     405
     406            // Switch back into select mode
     407            restoreCursor();
     408            Main.map.mapView.removeTemporaryLayer(this);
     409            selectedSegment = null;
     410            mode = Mode.select;
     411
     412            updateStatusLine();
     413            Main.map.mapView.repaint();
    253414        }
    254 
    255         Main.map.mapView.removeTemporaryLayer(this);
    256         selectedSegment = null;
    257         mode = null;
    258         updateStatusLine();
    259         Main.map.mapView.repaint();
    260415    }
    261416
    262417    @Override public String getModeHelpText() {
    263         if (mode == Mode.select)
    264             return tr("Release the mouse button to select the objects in the rectangle.");
    265         else if (mode == Mode.EXTRUDE)
     418        if (mode == Mode.translate)
     419            return tr("Move a segment along its normal, then release the mouse button.");
     420        else if (mode == Mode.extrude)
    266421            return tr("Draw a rectangle of the desired size, then release the mouse button.");
    267         else if (mode == Mode.rotate)
    268             return tr("Release the mouse button to stop rotating.");
    269422        else
    270             return tr("Drag a way segment to make a rectangle.");
     423            return tr("Drag a way segment to make a rectangle. Ctrl-drag to move a segment along its normal.");
    271424    }
    272425
    273426    @Override public boolean layerIsSupported(Layer l) {