Ticket #67: parallel-way-action.0.2.patch

File parallel-way-action.0.2.patch, 25.9 KB (added by olejorgenb, 12 months ago)

Select ways from mode, simple snap, copy tags, works on multiple (simple connected) ways

  • new file src/org/openstreetmap/josm/actions/mapmode/ParallelWayAction.java

    diff --git a/images/mapmode/parallel.png b/images/mapmode/parallel.png
    new file mode 100644
    index 0000000..a02c6fc
    Binary files /dev/null and b/images/mapmode/parallel.png differ
    diff --git a/src/org/openstreetmap/josm/actions/mapmode/ParallelWayAction.java b/src/org/openstreetmap/josm/actions/mapmode/ParallelWayAction.java
    new file mode 100644
    index 0000000..6aa149e
    - +  
     1// License: GPL. Copyright 2007 by Immanuel Scholz and others 
     2 
     3package org.openstreetmap.josm.actions.mapmode; 
     4 
     5import static org.openstreetmap.josm.gui.help.HelpUtil.ht; 
     6import static org.openstreetmap.josm.tools.I18n.tr; 
     7 
     8import java.awt.AWTEvent; 
     9import java.awt.Cursor; 
     10import java.awt.Point; 
     11import java.awt.Toolkit; 
     12import java.awt.event.AWTEventListener; 
     13import java.awt.event.ActionEvent; 
     14import java.awt.event.InputEvent; 
     15import java.awt.event.KeyEvent; 
     16import java.awt.event.MouseEvent; 
     17import java.util.ArrayList; 
     18import java.util.Collections; 
     19import java.util.HashMap; 
     20import java.util.List; 
     21 
     22import org.openstreetmap.josm.Main; 
     23import org.openstreetmap.josm.actions.CombineWayAction; 
     24import org.openstreetmap.josm.command.AddCommand; 
     25import org.openstreetmap.josm.command.Command; 
     26import org.openstreetmap.josm.command.SequenceCommand; 
     27import org.openstreetmap.josm.data.coor.EastNorth; 
     28import org.openstreetmap.josm.data.osm.DataSet; 
     29import org.openstreetmap.josm.data.osm.Node; 
     30import org.openstreetmap.josm.data.osm.OsmPrimitive; 
     31import org.openstreetmap.josm.data.osm.Way; 
     32import org.openstreetmap.josm.data.osm.WaySegment; 
     33import org.openstreetmap.josm.gui.MapFrame; 
     34import org.openstreetmap.josm.gui.MapView; 
     35import org.openstreetmap.josm.gui.layer.Layer; 
     36import org.openstreetmap.josm.gui.layer.OsmDataLayer; 
     37import org.openstreetmap.josm.tools.Geometry; 
     38import org.openstreetmap.josm.tools.Shortcut; 
     39 
     40//// TODO: (list below) 
     41/* 
     42 * 1. Use selected nodes as split points for the selected ways. 
     43 *  
     44 * The ways containing the selected nodes will be split and only the "inner" 
     45 * parts will be copied 
     46 *  
     47 * 2. Enter exact offset 
     48 *  
     49 * 3. Improve snapping 
     50 *  
     51 * Need at least a setting for step length 
     52 *  
     53 * 4. Visual cues? Highlight source path, draw offset line, etc? 
     54 *  
     55 * 5. Cursors 
     56 *  
     57 * 6. (long term) Parallelize and adjust offsets of existing ways 
     58 */ 
     59 
     60/** 
     61 * MapMode for making parallel ways. 
     62 *  
     63 * All calculations are done in projected coordinates. 
     64 *  
     65 * @author Ole JÞrgen BrÞnner (olejorgenb) 
     66 */ 
     67public class ParallelWayAction extends MapMode implements AWTEventListener { 
     68    // omg.. 
     69    public void dumpMouseEvent(MouseEvent e) { 
     70        //        System.out.println(e.paramString()); 
     71        //        System.out.print("e.getButton() = "); 
     72        //        switch (e.getButton()) { 
     73        //        case MouseEvent.BUTTON1: 
     74        //            System.out.println("BUTTON1"); 
     75        //            break; 
     76        //        case MouseEvent.BUTTON2: 
     77        //            System.out.println("BUTTON2"); 
     78        //            break; 
     79        //        case MouseEvent.BUTTON3: 
     80        //            System.out.println("BUTTON3"); 
     81        //            break; 
     82        //        case MouseEvent.NOBUTTON: 
     83        //            System.out.println("NOBUTTON"); 
     84        //            break; 
     85        //        default: 
     86        //            System.out.println(e.getButton()); 
     87        //            break; 
     88        //        } 
     89    } 
     90 
     91    private enum Mode { 
     92        dragging, normal 
     93    } 
     94 
     95    private Mode mode; 
     96 
     97    private boolean snap; 
     98    private double snapThreshold; 
     99 
     100    private int initialMoveDelay; 
     101    //    private int initialMoveThreshold; 
     102 
     103    private final MapView mv; 
     104 
     105    ParallelWayAction.MouseTracker mouseTracker = new MouseTracker(); 
     106 
     107    private WaySegment referenceSegment; 
     108    private ParallelWays pWays; 
     109 
     110    private boolean ctrl; 
     111    private boolean alt; 
     112    private boolean shift; 
     113 
     114    public ParallelWayAction(MapFrame mapFrame) { 
     115        super(tr("Parallel"), "parallel", tr("Makes a paralell copy of the selected way(s)"), Shortcut 
     116                .registerShortcut("mapmode:parallel", tr("Mode: {0}", tr("Parallel")), KeyEvent.VK_P, 
     117                        Shortcut.GROUP_EDIT, Shortcut.SHIFT_DEFAULT), mapFrame, Cursor 
     118                        .getPredefinedCursor(Cursor.MOVE_CURSOR)); 
     119        putValue("help", ht("/Action/Parallel")); 
     120        mv = mapFrame.mapView; 
     121 
     122        mouseTracker = new MouseTracker(); 
     123    } 
     124 
     125    @Override 
     126    public String getModeHelpText() { 
     127        // TODO: add more detailed feedback based on modifier state. 
     128        switch (mode) { 
     129        case normal: 
     130            return tr("Select ways as in Select mode. Drag selected ways or a single way to create a parallel copy (Alt for tagless)"); 
     131        case dragging: 
     132            return tr("Hold Ctrl to snap to whole meters"); 
     133        } 
     134        return ""; // impossible .. 
     135    } 
     136 
     137    @Override 
     138    public void enterMode() { 
     139        setMode(Mode.normal); 
     140        pWays = null; 
     141        super.enterMode(); 
     142        mv.addMouseListener(this); 
     143        mv.addMouseMotionListener(this); 
     144        // FIXME: What do do with default values? Not that they probably change 
     145        //        that often, but having them multiple places is still a bit ickky. 
     146        initialMoveDelay = Main.pref.getInteger("edit.initial-move-delay", 200); 
     147        //        initialMoveThreshold = Main.pref.getInteger("edit.initial-move-threshold", 5); 
     148        snapThreshold = Main.pref.getDouble("edit.make-parallel-way-action.snap-threshold", 0.35); 
     149 
     150        //// Needed to update the mouse cursor if modifiers are changed when the mouse is motionless 
     151        try { 
     152            Toolkit.getDefaultToolkit().addAWTEventListener(this, AWTEvent.KEY_EVENT_MASK); 
     153        } catch (SecurityException ex) { 
     154        } 
     155 
     156    } 
     157 
     158    @Override 
     159    public void exitMode() { 
     160        super.exitMode(); 
     161        mv.removeMouseListener(this); 
     162        mv.removeMouseMotionListener(this); 
     163        Main.map.statusLine.setDist(-1); 
     164        Main.map.statusLine.repaint(); 
     165        try { 
     166            Toolkit.getDefaultToolkit().removeAWTEventListener(this); 
     167        } catch (SecurityException ex) { 
     168        } 
     169    } 
     170 
     171    @Override 
     172    public boolean layerIsSupported(Layer layer) { 
     173        return layer instanceof OsmDataLayer; 
     174    } 
     175 
     176    @Override 
     177    public void eventDispatched(AWTEvent e) { 
     178        if (Main.map == null || mv == null || !mv.isActiveLayerDrawable()) 
     179            return; 
     180 
     181        // Should only get InputEvents due to the mask in enterMode 
     182        updateKeyModifiers((InputEvent) e); 
     183    } 
     184 
     185    private void updateKeyModifiers(InputEvent e) { 
     186        ctrl = (e.getModifiers() & ActionEvent.CTRL_MASK) != 0; 
     187        alt = (e.getModifiers() & (ActionEvent.ALT_MASK | InputEvent.ALT_GRAPH_MASK)) != 0; 
     188        shift = (e.getModifiers() & ActionEvent.SHIFT_MASK) != 0; 
     189    } 
     190 
     191    private void updateCursor(Mode mode) { 
     192 
     193    } 
     194 
     195    private void setMode(Mode mode) { 
     196        this.mode = mode; 
     197        updateCursor(this.mode); 
     198        updateStatusLine(); 
     199    } 
     200 
     201    private void onModifiersChanged() { 
     202        updateCursor(mode); 
     203    } 
     204 
     205    @Override 
     206    public void mousePressed(MouseEvent e) { 
     207        mouseTracker.registrerPressed(e); 
     208        dumpMouseEvent(e); 
     209        //        System.out.println("MousePressed"); 
     210        if (e.getButton() != MouseEvent.BUTTON1) 
     211            return; 
     212 
     213        if (!mv.isActiveLayerVisible()) 
     214            return; 
     215        if (!mv.isActiveLayerDrawable()) 
     216            return; 
     217        if (!(Boolean) this.getValue("active")) 
     218            return; 
     219    } 
     220 
     221    @Override 
     222    public void mouseClicked(MouseEvent e) { 
     223        //        System.out.println("MouseClicked"); 
     224    } 
     225 
     226    @Override 
     227    public void mouseReleased(MouseEvent e) { 
     228        mouseTracker.registrerReleased(e); 
     229        dumpMouseEvent(e); 
     230        //        System.out.println("MouseReleased"); 
     231        if (e.getButton() != MouseEvent.BUTTON1) 
     232            return; 
     233 
     234        updateKeyModifiers(e); 
     235 
     236        pWays = null; 
     237        setMode(Mode.normal); 
     238 
     239        if (!(mouseTracker.buttonState[MouseEvent.BUTTON1].hasBeenDragged)) { 
     240            assert (pWays == null); 
     241            // use point from press or click event? (or are these always the same) 
     242            Way nearestWay = mv.getNearestWay(e.getPoint(), OsmPrimitive.isSelectablePredicate); 
     243            if (nearestWay == null) { 
     244                if (!shift && !ctrl) { 
     245                    getCurrentDataSet().clearSelection(); 
     246                } 
     247                return; 
     248            } 
     249            boolean isSelected = nearestWay.isSelected(); 
     250            if (shift && !ctrl && !alt) { 
     251                if (!isSelected) { 
     252                    getCurrentDataSet().addSelected(nearestWay); 
     253                } 
     254            } else if (ctrl && !shift && !alt) { 
     255                if (isSelected) { 
     256                    getCurrentDataSet().clearSelection(nearestWay); 
     257                } else { 
     258                    getCurrentDataSet().addSelected(nearestWay); 
     259                } 
     260            } else if (!ctrl && !shift && !alt) { 
     261                getCurrentDataSet().setSelected(nearestWay); 
     262            } // else -> invalid modifier combination 
     263        } 
     264    } 
     265 
     266    @Override 
     267    public void mouseDragged(MouseEvent e) { 
     268        mouseTracker.registrerDragged(e); 
     269        dumpMouseEvent(e); 
     270        //        System.out.println("MouseDragged"); 
     271        if (!mouseTracker.buttonState[MouseEvent.BUTTON1].isPressed) 
     272            return; // WTF.. the events passed here does not have button info? 
     273 
     274        updateKeyModifiers(e); 
     275 
     276        if ((System.currentTimeMillis() - mouseTracker.buttonState[MouseEvent.BUTTON1].pressedTime) < initialMoveDelay) 
     277            return; 
     278 
     279        if (!shift && !ctrl) { 
     280            snap = false; 
     281        } else if (!shift && ctrl) { 
     282            snap = true; 
     283        } 
     284 
     285        Point p = e.getPoint(); 
     286        if (pWays == null) { 
     287            boolean copyTags = true; 
     288            // This is the first drag event 
     289            if (!shift && alt) { 
     290                copyTags = false; 
     291            } else if (!shift && !alt) { 
     292                copyTags = true; 
     293            } else 
     294                return; // invalid modifier combinations 
     295            // Important to use mouse position from the press, since the drag 
     296            // event can come quite late 
     297            if (!initParallelWays(mouseTracker.buttonState[MouseEvent.BUTTON1].pressedPos, copyTags)) 
     298                return; 
     299            setMode(Mode.dragging); 
     300        } 
     301 
     302        //// Calculate distance to the reference line 
     303        EastNorth enp = mv.getEastNorth((int) p.getX(), (int) p.getY()); 
     304        EastNorth enOnRefLine = Geometry.closestPointToLine(referenceSegment.getFirstNode().getEastNorth(), 
     305                referenceSegment.getSecondNode().getEastNorth(), enp); 
     306        double d = enp.distance(enOnRefLine); 
     307        boolean toTheRight = Geometry.isToTheRightSideOfLine(referenceSegment.getFirstNode(), 
     308                referenceSegment.getFirstNode(), referenceSegment.getSecondNode(), new Node(enp)); 
     309 
     310        if (snap) { 
     311            // TODO: Very simple snapping 
     312            // - Snap steps and/or threshold relative to the distance? 
     313            long closestWholeUnit = Math.round(d); 
     314            if (Math.abs(closestWholeUnit - d) < snapThreshold) { 
     315                d = closestWholeUnit; 
     316            } else { 
     317                d = closestWholeUnit + Math.signum(closestWholeUnit - d) * -0.5; 
     318            } 
     319        } 
     320        if (toTheRight) { 
     321            d = -d; 
     322        } 
     323        pWays.changeOffset(d); 
     324 
     325        Main.map.statusLine.setDist(Math.abs(d)); 
     326        Main.map.statusLine.repaint(); 
     327        mv.repaint(); 
     328    } 
     329 
     330    // TODO: rename 
     331    private boolean initParallelWays(Point p, boolean copyTags) { 
     332        referenceSegment = mv.getNearestWaySegment(p, Way.isUsablePredicate, true); 
     333        if (referenceSegment == null) 
     334            return false; 
     335 
     336        // The collection returned is very inefficient so we collect it in an ArrayList 
     337        // Not sure if the list is iterated multiple times any more... 
     338        List<Way> selectedWays = new ArrayList<Way>(getCurrentDataSet().getSelectedWays()); 
     339        if (!selectedWays.contains(referenceSegment.way)) { 
     340            getCurrentDataSet().setSelected(referenceSegment.way); 
     341            selectedWays.clear(); 
     342            selectedWays.add(referenceSegment.way); 
     343        } 
     344 
     345        try { 
     346            pWays = new ParallelWays(selectedWays, copyTags, selectedWays.indexOf(referenceSegment.way)); 
     347            pWays.commit(null); 
     348            getCurrentDataSet().setSelected(pWays.ways); 
     349            return true; 
     350        } catch (IllegalArgumentException e) { 
     351            System.err.println(e); 
     352            pWays = null; 
     353            return false; 
     354        } 
     355    } 
     356 
     357    // TODO: 'ParallelPath' better name? 
     358    static final class ParallelWays { 
     359        private List<Way> ways; 
     360        private List<Node> sortedNodes; 
     361 
     362        private int nodeCount; 
     363 
     364        private EastNorth[] pts; 
     365        private EastNorth[] normals; 
     366 
     367        public ParallelWays(List<Way> sourceWays, boolean copyTags, int refWayIndex) { 
     368            // Possible/sensible to use PrimetiveDeepCopy here? 
     369 
     370            //// Make a deep copy of the ways, keeping the copied ways connected 
     371            HashMap<Node, Node> splitNodeMap = new HashMap<Node, Node>(sourceWays.size()); 
     372            for (Way w : sourceWays) { 
     373                if (!splitNodeMap.containsKey(w.firstNode())) { 
     374                    splitNodeMap.put(w.firstNode(), copyNode(w.firstNode(), copyTags)); 
     375                } 
     376                if (!splitNodeMap.containsKey(w.lastNode())) { 
     377                    splitNodeMap.put(w.lastNode(), copyNode(w.lastNode(), copyTags)); 
     378                } 
     379            } 
     380            ways = new ArrayList<Way>(sourceWays.size()); 
     381            for (Way w : sourceWays) { 
     382                Way wCopy = new Way(); 
     383                wCopy.addNode(splitNodeMap.get(w.firstNode())); 
     384                for (int i = 1; i < w.getNodesCount() - 1; i++) { 
     385                    wCopy.addNode(copyNode(w.getNode(i), copyTags)); 
     386                } 
     387                wCopy.addNode(splitNodeMap.get(w.lastNode())); 
     388                if (copyTags) { 
     389                    wCopy.setKeys(w.getKeys()); 
     390                } 
     391                ways.add(wCopy); 
     392            } 
     393            sourceWays = null; // Ensure that we only use the copies from now 
     394 
     395            //// Find a linear ordering of the nodes. Fail if there isn't one. 
     396            CombineWayAction.NodeGraph nodeGraph = CombineWayAction.NodeGraph.createUndirectedGraphFromNodeWays(ways); 
     397            sortedNodes = nodeGraph.buildSpanningPath(); 
     398            if (sortedNodes == null) 
     399                throw new IllegalArgumentException("Ways must have spanning path"); // Create a dedicated exception? 
     400 
     401            //// Ugly method of ensuring that the offset isn't inverted. I'm sure there is a better and more elegant way, but I'm starting to get sleepy, so I do this for now. 
     402            { 
     403                Way refWay = ways.get(refWayIndex); 
     404                boolean refWayReversed = false; 
     405                if (isClosedPath()) { // Nodes occur more than once in the list 
     406                    if (refWay.firstNode() == sortedNodes.get(0) && refWay.lastNode() == sortedNodes.get(0)) { 
     407                        refWayReversed = sortedNodes.get(1) != refWay.getNode(1); 
     408                    } else if (refWay.lastNode() == sortedNodes.get(0)) { 
     409                        refWayReversed = 
     410                            sortedNodes.get(sortedNodes.size() - 1) != refWay.getNode(refWay.getNodesCount() - 1); 
     411                    } else if (refWay.firstNode() == sortedNodes.get(0)) { 
     412                        refWayReversed = sortedNodes.get(1) != refWay.getNode(1); 
     413                    } else { 
     414                        refWayReversed = 
     415                            sortedNodes.indexOf(refWay.firstNode()) > sortedNodes.indexOf(refWay.lastNode()); 
     416                    } 
     417 
     418                } else { 
     419                    refWayReversed = sortedNodes.indexOf(refWay.firstNode()) > sortedNodes.indexOf(refWay.lastNode()); 
     420                } 
     421                if (refWayReversed) { 
     422                    Collections.reverse(sortedNodes); // need to keep the orientation of the reference way. 
     423                    System.err.println("reversed!"); 
     424                } 
     425            } 
     426 
     427            //// Initialize the required parameters. (segment normals, etc.) 
     428            nodeCount = sortedNodes.size(); 
     429            pts = new EastNorth[nodeCount]; 
     430            normals = new EastNorth[nodeCount - 1]; 
     431            int i = 0; 
     432            for (Node n : sortedNodes) { 
     433                EastNorth t = n.getEastNorth(); 
     434                pts[i] = t; 
     435                i++; 
     436            } 
     437            for (i = 0; i < nodeCount - 1; i++) { 
     438                double dx = pts[i + 1].getX() - pts[i].getX(); 
     439                double dy = pts[i + 1].getY() - pts[i].getY(); 
     440                double len = Math.sqrt(dx * dx + dy * dy); 
     441                normals[i] = new EastNorth(-dy / len, dx / len); 
     442            } 
     443        } 
     444 
     445        public boolean isClosedPath() { 
     446            return sortedNodes.get(0) == sortedNodes.get(sortedNodes.size() - 1); 
     447        } 
     448 
     449        public void changeOffset(double d) { 
     450            //// This is the core algorithm: 
     451            /* 1. Calculate a parallel line, offset by 'd', to each segment in 
     452             *    the path 
     453             * 2. Find the intersection of lines belonging to neighboring 
     454             *    segments. These become the new node positions 
     455             * 3. Do some special casing for closed paths 
     456             *  
     457             * Simple and probably not even close to optimal performance-vise 
     458             */ 
     459 
     460            EastNorth[] ppts = new EastNorth[nodeCount]; 
     461 
     462            EastNorth prevA = add(pts[0], mul(normals[0], d)); 
     463            EastNorth prevB = add(pts[1], mul(normals[0], d)); 
     464            for (int i = 1; i < nodeCount - 1; i++) { 
     465                EastNorth A = add(pts[i], mul(normals[i], d)); 
     466                EastNorth B = add(pts[i + 1], mul(normals[i], d)); 
     467                if (Geometry.segmentsParallel(A, B, prevA, prevB)) { 
     468                    ppts[i] = A; 
     469                } else { 
     470                    ppts[i] = Geometry.getLineLineIntersection(A, B, prevA, prevB); 
     471                } 
     472                prevA = A; 
     473                prevB = B; 
     474            } 
     475            if (isClosedPath()) { 
     476                EastNorth A = add(pts[0], mul(normals[0], d)); 
     477                EastNorth B = add(pts[1], mul(normals[0], d)); 
     478                if (Geometry.segmentsParallel(A, B, prevA, prevB)) { 
     479                    ppts[0] = A; 
     480                } else { 
     481                    ppts[0] = Geometry.getLineLineIntersection(A, B, prevA, prevB); 
     482                } 
     483                ppts[nodeCount - 1] = ppts[0]; 
     484            } else { 
     485                ppts[0] = add(pts[0], mul(normals[0], d)); 
     486                ppts[nodeCount - 1] = add(pts[nodeCount - 1], mul(normals[nodeCount - 2], d)); 
     487            } 
     488 
     489            for (int i = 0; i < nodeCount; i++) { 
     490                sortedNodes.get(i).setEastNorth(ppts[i]); 
     491            } 
     492        } 
     493 
     494        // Draw helper lines instead like DrawAction ExtrudeAction? 
     495        public void commit(DataSet ds) { 
     496            SequenceCommand undoCommand = new SequenceCommand("Make parallel way(s)", makeAddWayAndNodesCommandList()); 
     497            Main.main.undoRedo.add(undoCommand); 
     498        } 
     499 
     500        private List<Command> makeAddWayAndNodesCommandList() { 
     501            ArrayList<Command> commands = new ArrayList<Command>(sortedNodes.size() + ways.size()); 
     502            for (int i = 0; i < sortedNodes.size() - 1; i++) { 
     503                commands.add(new AddCommand(sortedNodes.get(i))); 
     504            } 
     505            if (!isClosedPath()) { 
     506                commands.add(new AddCommand(sortedNodes.get(sortedNodes.size() - 1))); 
     507            } 
     508            for (Way w : ways) { 
     509                commands.add(new AddCommand(w)); 
     510            } 
     511            return commands; 
     512        } 
     513 
     514        static private Node copyNode(Node source, boolean copyTags) { 
     515            if (copyTags) 
     516                return new Node(source, true); 
     517            else { 
     518                Node n = new Node(); 
     519                n.setCoor(source.getCoor()); 
     520                return n; 
     521            } 
     522        } 
     523 
     524        // We need either a dedicated vector type, or operations such as these 
     525        // added to EastNorth... 
     526        static private EastNorth mul(EastNorth en, double f) { 
     527            return new EastNorth(en.getX() * f, en.getY() * f); 
     528        } 
     529 
     530        static private EastNorth add(EastNorth a, EastNorth b) { 
     531            return new EastNorth(a.east() + b.east(), a.north() + b.north()); 
     532        } 
     533    } 
     534 
     535    // TODO: Finish me. (or maybe having a state-tracker object for this is over-complicating things?) 
     536    // Dunno.. this swing stuff is a mess.. 
     537    static final class MouseTracker { 
     538        public static final class ButtonState { 
     539            public long pressedTime = -1; 
     540            public Point pressedPos = null; 
     541            public long releasedTime = -1; 
     542            public Point releasedPos = null; 
     543            public boolean hasBeenDragged = false; 
     544            public boolean isPressed = false; 
     545            public Point lastPos = null; 
     546        } 
     547 
     548        public MouseTracker() { 
     549            for (int i = 0; i < buttonCount; i++) { 
     550                buttonState[i] = new ButtonState(); 
     551            } 
     552        } 
     553 
     554        public ButtonState[] buttonState = new ButtonState[4]; 
     555        public final int buttonCount = 4; 
     556 
     557        public MouseEvent lastPressedEvent; 
     558        public MouseEvent lastMoveEvent; 
     559        public MouseEvent lastDragEvent; 
     560        public MouseEvent lastReleaseEvent; 
     561 
     562        public ButtonState getButtonState(MouseEvent e) { 
     563            return buttonState[e.getButton()]; 
     564        } 
     565 
     566        public void registrerPressed(MouseEvent e) { 
     567            ButtonState b = getButtonState(e); 
     568            b.pressedTime = System.currentTimeMillis(); 
     569            b.pressedPos = e.getPoint(); 
     570            b.hasBeenDragged = false; 
     571            b.lastPos = e.getPoint(); 
     572            b.isPressed = true; 
     573 
     574            lastPressedEvent = e; 
     575        } 
     576 
     577        public void registrerReleased(MouseEvent e) { 
     578            ButtonState b = getButtonState(e); 
     579            b.releasedTime = System.currentTimeMillis(); 
     580            b.releasedPos = e.getPoint(); 
     581            b.isPressed = false; 
     582            b.lastPos = e.getPoint(); 
     583 
     584            lastReleaseEvent = e; 
     585        } 
     586 
     587        public void registrerMoved(MouseEvent e) { 
     588            // TODO: 
     589        } 
     590 
     591        public void registrerDragged(MouseEvent e) { 
     592            // no button info 
     593            for (ButtonState b : buttonState) { 
     594                if (b.isPressed) { 
     595                    b.hasBeenDragged = true; 
     596                    b.lastPos = e.getPoint(); 
     597                } 
     598            } 
     599 
     600            lastDragEvent = e; 
     601        } 
     602 
     603        public void clear() { 
     604        } 
     605    } 
     606} 
  • src/org/openstreetmap/josm/gui/MapFrame.java

    diff --git a/src/org/openstreetmap/josm/gui/MapFrame.java b/src/org/openstreetmap/josm/gui/MapFrame.java
    index 96bddae..830c567 100644
    a b import org.openstreetmap.josm.actions.mapmode.DeleteAction; 
    4040import org.openstreetmap.josm.actions.mapmode.DrawAction; 
    4141import org.openstreetmap.josm.actions.mapmode.ExtrudeAction; 
    4242import org.openstreetmap.josm.actions.mapmode.MapMode; 
     43import org.openstreetmap.josm.actions.mapmode.ParallelWayAction; 
    4344import org.openstreetmap.josm.actions.mapmode.SelectAction; 
    4445import org.openstreetmap.josm.actions.mapmode.ZoomAction; 
    4546import org.openstreetmap.josm.gui.MapView.LayerChangeListener; 
    public class MapFrame extends JPanel implements Destroyable, LayerChangeListener 
    129130        addMapMode(new IconToggleButton(new SelectAction(this))); 
    130131        addMapMode(new IconToggleButton(new DrawAction(this))); 
    131132        addMapMode(new IconToggleButton(new ExtrudeAction(this))); 
     133        addMapMode(new IconToggleButton(new ParallelWayAction(this))); 
    132134        addMapMode(new IconToggleButton(new ZoomAction(this))); 
    133135        addMapMode(new IconToggleButton(new DeleteAction(this))); 
    134136 
  • src/org/openstreetmap/josm/tools/Geometry.java

    diff --git a/src/org/openstreetmap/josm/tools/Geometry.java b/src/org/openstreetmap/josm/tools/Geometry.java
    index ade8359..9930fbb 100644
    a b public class Geometry { 
    332332        else 
    333333            return new EastNorth(segmentP1.getX() + ldx * offset, segmentP1.getY() + ldy * offset); 
    334334    } 
     335    public static EastNorth closestPointToLine(EastNorth lineP1, EastNorth lineP2, EastNorth point) { 
     336        double ldx = lineP2.getX() - lineP1.getX(); 
     337        double ldy = lineP2.getY() - lineP1.getY(); 
     338 
     339        if (ldx == 0 && ldy == 0) //segment zero length 
     340            return lineP1; 
     341 
     342        double pdx = point.getX() - lineP1.getX(); 
     343        double pdy = point.getY() - lineP1.getY(); 
     344 
     345        double offset = (pdx * ldx + pdy * ldy) / (ldx * ldx + ldy * ldy); 
     346        return new EastNorth(lineP1.getX() + ldx * offset, lineP1.getY() + ldy * offset); 
     347    } 
    335348 
    336349    /** 
    337350     * This method tests if secondNode is clockwise to first node. 
    public class Geometry { 
    457470 
    458471        return inside; 
    459472    } 
    460      
     473 
    461474    /** 
    462475     * returns area of a closed way in square meters 
    463476     * (approximate(?), but should be OK for small areas) 
    public class Geometry { 
    477490        } 
    478491        return Math.abs(area/2); 
    479492    } 
    480      
     493 
    481494    protected static double calcX(Node p1){ 
    482495        double lat1, lon1, lat2, lon2; 
    483496        double dlon, dlat; 
    public class Geometry { 
    494507        double c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a)); 
    495508        return 6367000 * c; 
    496509    } 
    497      
     510 
    498511    protected static double calcY(Node p1){ 
    499512        double lat1, lon1, lat2, lon2; 
    500513        double dlon, dlat;