Ticket #5179: osm-join-areas-5.patch

File osm-join-areas-5.patch, 25.0 KB (added by extropy, 21 months ago)

Improved patch - preserves path orientation when possible.

  • src/org/openstreetmap/josm/actions/CombineWayAction.java

     
    1414import java.util.Collections; 
    1515import java.util.HashMap; 
    1616import java.util.HashSet; 
     17import java.util.LinkedHashMap; 
     18import java.util.LinkedHashSet; 
    1719import java.util.LinkedList; 
    1820import java.util.List; 
    1921import java.util.Set; 
     
    112114        if (ways == null || ways.isEmpty()) 
    113115            return null; 
    114116        ways.remove(null); // just in case -  remove all null ways from the collection 
    115         ways = new HashSet<Way>(ways); // remove duplicates 
    116117 
     118        // remove duplicates, preserving order 
     119        ways = new LinkedHashSet<Way>(ways); 
     120 
    117121        // try to build a new way which includes all the combined 
    118122        // ways 
    119123        // 
     
    473477        } 
    474478 
    475479        protected void prepare() { 
    476             Set<NodePair> undirectedEdges = new HashSet<NodePair>(); 
    477             successors = new HashMap<Node, List<NodePair>>(); 
    478             predecessors = new HashMap<Node, List<NodePair>>(); 
     480            Set<NodePair> undirectedEdges = new LinkedHashSet<NodePair>(); 
     481            successors = new LinkedHashMap<Node, List<NodePair>>(); 
     482            predecessors = new LinkedHashMap<Node, List<NodePair>>(); 
    479483 
    480484            for (NodePair pair: edges) { 
    481485                if (!undirectedEdges.contains(pair) && ! undirectedEdges.contains(pair.swap())) { 
     
    488492        } 
    489493 
    490494        public NodeGraph() { 
    491             edges = new HashSet<NodePair>(); 
     495            edges = new LinkedHashSet<NodePair>(); 
    492496        } 
    493497 
    494498        public void add(NodePair pair) { 
     
    513517        } 
    514518 
    515519        protected Set<Node> getTerminalNodes() { 
    516             Set<Node> ret = new HashSet<Node>(); 
     520            Set<Node> ret = new LinkedHashSet<Node>(); 
    517521            for (Node n: getNodes()) { 
    518522                if (isTerminalNode(n)) { 
    519523                    ret.add(n); 
     
    523527        } 
    524528 
    525529        protected Set<Node> getNodes(Stack<NodePair> pairs) { 
    526             HashSet<Node> nodes = new HashSet<Node>(); 
     530            HashSet<Node> nodes = new LinkedHashSet<Node>(); 
    527531            for (NodePair pair: pairs) { 
    528532                nodes.add(pair.getA()); 
    529533                nodes.add(pair.getB()); 
     
    543547        } 
    544548 
    545549        protected Set<Node> getNodes() { 
    546             Set<Node> nodes = new HashSet<Node>(); 
     550            Set<Node> nodes = new LinkedHashSet<Node>(); 
    547551            for (NodePair pair: edges) { 
    548552                nodes.add(pair.getA()); 
    549553                nodes.add(pair.getB()); 
  • src/org/openstreetmap/josm/actions/JoinAreasAction.java

     
    66import static org.openstreetmap.josm.tools.I18n.trn; 
    77 
    88import java.awt.GridBagLayout; 
    9 import java.awt.Polygon; 
    109import java.awt.event.ActionEvent; 
    1110import java.awt.event.KeyEvent; 
    1211import java.awt.geom.Area; 
     
    1514import java.util.Collection; 
    1615import java.util.Collections; 
    1716import java.util.HashMap; 
     17import java.util.HashSet; 
    1818import java.util.LinkedList; 
    1919import java.util.List; 
    2020import java.util.Map; 
     
    3030import javax.swing.JPanel; 
    3131 
    3232import org.openstreetmap.josm.Main; 
     33import org.openstreetmap.josm.actions.SplitWayAction.SplitWayResult; 
    3334import org.openstreetmap.josm.command.AddCommand; 
    3435import org.openstreetmap.josm.command.ChangeCommand; 
    3536import org.openstreetmap.josm.command.Command; 
     
    109110        } 
    110111    } 
    111112 
     113    //HelperClass 
     114    //saves a way and it's orientation. 
     115    private static class WayInPath{ 
     116        public final Way way; 
     117        public boolean reverse; 
     118 
     119        public WayInPath(Way _way, boolean _reverse){ 
     120            this.way = _way; 
     121            this.reverse = _reverse; 
     122        } 
     123 
     124    } 
     125 
    112126    // Adds the menu entry, Shortcuts, etc. 
    113127    public JoinAreasAction() { 
    114128        super(tr("Join overlapping Areas"), "joinareas", tr("Joins areas that overlap each other"), Shortcut.registerShortcut("tools:joinareas", tr("Tool: {0}", tr("Join overlapping Areas")), 
     
    185199        if(!same) { 
    186200            int i = 0; 
    187201            if(checkForTagConflicts(a, b)) return true; // User aborted, so don't warn again 
     202 
     203            //join each area with itself, fixing self-crossings. 
    188204            if(joinAreas(a, a)) { 
    189205                ++i; 
    190206            } 
     
    195211            cmdsCount = i; 
    196212        } 
    197213 
    198         ArrayList<OsmPrimitive> nodes = addIntersections(a, b); 
     214        ArrayList<Node> nodes = addIntersections(a, b); 
    199215        if(nodes.size() == 0) return hadChanges; 
    200216        commitCommands(marktr("Added node on all intersections")); 
    201217 
     
    208224        // Don't warn now, because it will really look corrupted 
    209225        boolean warnAboutRelations = relations.size() > 0; 
    210226 
    211         Collection<Way> allWays = splitWaysOnNodes(a, b, nodes); 
     227        ArrayList<Way> allWays = splitWaysOnNodes(a, b, nodes); 
    212228 
    213         // Find all nodes and inner ways save them to a list 
    214         Collection<Node> allNodes = getNodesFromWays(allWays); 
    215         Collection<Way> innerWays = findInnerWays(allWays, allNodes); 
     229        // Find inner ways save them to a list 
     230        ArrayList<WayInPath> outerWays = findOuterWays(allWays); 
     231        ArrayList<Way> innerWays = findInnerWays(allWays, outerWays); 
    216232 
    217233        // Join outer ways 
    218         Way outerWay = joinOuterWays(allWays, innerWays); 
     234        Way outerWay = joinOuterWays(outerWays); 
    219235        if (outerWay == null) 
    220236            return true; 
    221237 
     
    330346     * @param Way Second way 
    331347     * @return ArrayList<OsmPrimitive> List of new nodes 
    332348     */ 
    333     private ArrayList<OsmPrimitive> addIntersections(Way a, Way b) { 
     349    private ArrayList<Node> addIntersections(Way a, Way b) { 
    334350        boolean same = a.equals(b); 
    335351        int nodesSizeA = a.getNodesCount(); 
    336352        int nodesSizeB = b.getNodesCount(); 
    337353 
    338         // We use OsmPrimitive here instead of Node because we later need to split a way at these nodes. 
    339         // With OsmPrimitve we can simply add the way and don't have to loop over the nodes 
    340         ArrayList<OsmPrimitive> nodes = new ArrayList<OsmPrimitive>(); 
     354        ArrayList<Node> nodes = new ArrayList<Node>(); 
    341355        ArrayList<NodeToSegs> nodesA = new ArrayList<NodeToSegs>(); 
    342356        ArrayList<NodeToSegs> nodesB = new ArrayList<NodeToSegs>(); 
    343357 
     
    488502    } 
    489503 
    490504    /** 
    491      * This is a hacky implementation to make use of the splitWayAction code and 
    492      * should be improved. SplitWayAction needs to expose its splitWay function though. 
     505     * This is a method splits ways into smaller parts, using the prepared nodes list as split points. 
     506     * Uses  SplitWayAction.splitWay for the heavy lifting. 
     507     * @return list of split ways (or original ways if no splitting is done). 
    493508     */ 
    494     private Collection<Way> splitWaysOnNodes(Way a, Way b, Collection<OsmPrimitive> nodes) { 
    495         ArrayList<Way> ways = new ArrayList<Way>(); 
     509    private ArrayList<Way> splitWaysOnNodes(Way a, Way b, Collection<Node> nodes) { 
     510 
     511        ArrayList<Way> result = new ArrayList<Way>(); 
     512        List<Way> ways = new ArrayList<Way>(); 
    496513        ways.add(a); 
    497         if(!a.equals(b)) { 
    498             ways.add(b); 
     514        ways.add(b); 
     515 
     516        for (Way way: ways){ 
     517            List<List<Node>> chunks = buildNodeChunks(way, nodes); 
     518            SplitWayResult split = SplitWayAction.splitWay(Main.map.mapView.getEditLayer(), way, chunks, Collections.<OsmPrimitive>emptyList()); 
     519 
     520            //execute the command, we need the results 
     521            Main.main.undoRedo.add(split.getCommand()); 
     522            cmdsCount ++; 
     523 
     524            result.add(split.getOriginalWay()); 
     525            result.addAll(split.getNewWays()); 
    499526        } 
    500527 
    501         List<OsmPrimitive> affected = new ArrayList<OsmPrimitive>(); 
    502         for (Way way : ways) { 
    503             nodes.add(way); 
    504             Main.main.getCurrentDataSet().setSelected(nodes); 
    505             nodes.remove(way); 
    506             new SplitWayAction().actionPerformed(null); 
    507             cmdsCount++; 
    508             affected.addAll(Main.main.getCurrentDataSet().getSelectedWays()); 
    509         } 
    510         return osmprim2way(affected); 
     528        return result; 
    511529    } 
    512530 
     531 
    513532    /** 
    514      * Converts a list of OsmPrimitives to a list of Ways 
    515      * @param Collection<OsmPrimitive> The OsmPrimitives list that's needed as a list of Ways 
    516      * @return Collection<Way> The list as list of Ways 
     533     * Simple chunking version. Does not care about circular ways and result being proper, we will glue it all back together later on. 
     534     * @param way the way to chunk 
     535     * @param splitNodes the places where to cut. 
     536     * @return list of node segments to produce. 
    517537     */ 
    518     static private Collection<Way> osmprim2way(Collection<OsmPrimitive> ways) { 
    519         Collection<Way> result = new ArrayList<Way>(); 
    520         for(OsmPrimitive w: ways) { 
    521             if(w instanceof Way) { 
    522                 result.add((Way) w); 
     538    private List<List<Node>> buildNodeChunks(Way way, Collection<Node> splitNodes) 
     539    { 
     540        List<List<Node>> result = new ArrayList<List<Node>>(); 
     541        List<Node> curList = new ArrayList<Node>(); 
     542 
     543        for(Node node: way.getNodes()){ 
     544            curList.add(node); 
     545            if (curList.size() > 1 && splitNodes.contains(node)){ 
     546                result.add(curList); 
     547                curList = new ArrayList<Node>(); 
     548                curList.add(node); 
    523549            } 
    524550        } 
     551 
     552        if (curList.size() > 1) 
     553        { 
     554            result.add(curList); 
     555        } 
     556 
    525557        return result; 
    526558    } 
    527559 
     560 
    528561    /** 
    529562     * Returns all nodes for given ways 
    530563     * @param Collection<Way> The list of ways which nodes are to be returned 
     
    538571        return allNodes; 
    539572    } 
    540573 
     574 
    541575    /** 
    542      * Finds all inner ways for a given list of Ways and Nodes from a multigon by constructing a polygon 
    543      * for each way, looking for inner nodes that are not part of this way. If a node is found, all ways 
    544      * containing this node are added to the list 
     576     * Gets all inner ways given all ways and outer ways. 
     577     * @param multigonWays 
     578     * @param outerWays 
     579     * @return list of inner ways. 
     580     */ 
     581    private ArrayList<Way> findInnerWays(Collection<Way> multigonWays, Collection<WayInPath> outerWays) { 
     582        ArrayList<Way> innerWays = new ArrayList<Way>(); 
     583        Set<Way> outerSet = new HashSet<Way>(); 
     584 
     585        for(WayInPath w: outerWays) { 
     586            outerSet.add(w.way); 
     587        } 
     588 
     589        for(Way way: multigonWays) { 
     590            if (!outerSet.contains(way)) { 
     591                innerWays.add(way); 
     592            } 
     593        } 
     594 
     595        return innerWays; 
     596    } 
     597 
     598 
     599    /** 
     600     * Finds all ways for a given list of Ways that form the outer hull. 
     601     * This works by starting with one node and traversing the multigon clockwise, always picking the leftmost path. 
     602     * Prerequisites - the ways must not intersect and have common end nodes where they meet. 
    545603     * @param Collection<Way> A list of (splitted) ways that form a multigon 
    546      * @param Collection<Node> A list of nodes that belong to the multigon 
    547      * @return Collection<Way> A list of ways that are positioned inside the outer borders of the multigon 
     604     * @return Collection<Way> A list of ways that form the outer boundary of the multigon. 
    548605     */ 
    549     private Collection<Way> findInnerWays(Collection<Way> multigonWays, Collection<Node> multigonNodes) { 
    550         Collection<Way> innerWays = new ArrayList<Way>(); 
    551         for(Way w: multigonWays) { 
    552             Polygon poly = new Polygon(); 
    553             for(Node n: (w).getNodes()) { 
    554                 poly.addPoint(latlonToXY(n.getCoor().lat()), latlonToXY(n.getCoor().lon())); 
     606    public static ArrayList<WayInPath> findOuterWays(Collection<Way> multigonWays) { 
     607 
     608        //find the node with minimum lat - it's guaranteed to be outer. (What about the south pole?) 
     609        Way bestWay = null; 
     610        Node topNode = null; 
     611        int topIndex = 0; 
     612        double minLat = Double.POSITIVE_INFINITY; 
     613 
     614        for(Way way: multigonWays) { 
     615            for (int pos = 0; pos < way.getNodesCount(); pos ++){ 
     616                Node node = way.getNode(pos); 
     617 
     618                if (node.getCoor().lat() < minLat){ 
     619                    minLat = node.getCoor().lat(); 
     620                    bestWay = way; 
     621                    topNode = node; 
     622                    topIndex = pos; 
     623                } 
    555624            } 
     625        } 
    556626 
    557             for(Node n: multigonNodes) { 
    558                 if(!(w).containsNode(n) && poly.contains(latlonToXY(n.getCoor().lat()), latlonToXY(n.getCoor().lon()))) { 
    559                     getWaysByNode(innerWays, multigonWays, n); 
     627        //get two final nodes from best way to mark as starting point and orientation. 
     628        Node headNode = null; 
     629        Node prevNode = null; 
     630 
     631        if (topNode.equals(bestWay.firstNode()) || topNode.equals(bestWay.lastNode())) 
     632        { 
     633            //node is in split point 
     634            headNode = topNode; 
     635            //make a fake node that is downwards from head node (smaller latitude). It will be a division point between paths. 
     636            prevNode = new Node(new LatLon(headNode.getCoor().lat() - 1000, headNode.getCoor().lon())); 
     637        } 
     638        else 
     639        { 
     640            //node is inside way - pick the clockwise going end. 
     641            Node prev = bestWay.getNode(topIndex - 1); 
     642            Node next = bestWay.getNode(topIndex + 1); 
     643 
     644            if (angleIsClockwise(prev, topNode, next)){ 
     645                headNode = bestWay.lastNode(); 
     646                prevNode = bestWay.getNode(bestWay.getNodesCount() - 2); 
     647            } 
     648            else 
     649            { 
     650                headNode = bestWay.firstNode(); 
     651                prevNode = bestWay.getNode(1); 
     652            } 
     653        } 
     654 
     655        Set<Way> outerWays = new HashSet<Way>(); 
     656        ArrayList<WayInPath> result = new ArrayList<WayInPath>(); 
     657 
     658        //iterate till full circle is reached 
     659        while (true){ 
     660 
     661            bestWay = null; 
     662            Node bestWayNextNode = null; 
     663            boolean bestWayReverse = false; 
     664 
     665            for (Way way: multigonWays) 
     666            { 
     667                boolean wayReverse; 
     668                Node nextNode; 
     669 
     670                if (way.firstNode().equals(headNode)){ 
     671                    nextNode = way.getNode(1); 
     672                    wayReverse = false; 
    560673                } 
     674                else if (way.lastNode().equals(headNode)) 
     675                { 
     676                    nextNode = way.getNode(way.getNodesCount() - 2); 
     677                    wayReverse = true; 
     678                } 
     679                else 
     680                { 
     681                    //this way not adjacent to headNode 
     682                    continue; 
     683                } 
     684 
     685                if (nextNode.equals(prevNode)) 
     686                { 
     687                    //this is the path we came from - ignore it. 
     688                } 
     689                else if (bestWay == null || !isToTheRightSideOfLine(prevNode, headNode, bestWayNextNode, nextNode)) 
     690                { 
     691                    //the new way is better 
     692                    bestWay = way; 
     693                    bestWayReverse = wayReverse; 
     694                    bestWayNextNode = nextNode; 
     695                } 
    561696            } 
     697 
     698            if (bestWay == null) 
     699                //this should not happen. Internal error here. 
     700                return null; 
     701            else if (outerWays.contains(bestWay)){ 
     702                //full circle reached, terminate. 
     703                break; 
     704            } 
     705            else 
     706            { 
     707                //add to outer ways, repeat. 
     708                outerWays.add(bestWay); 
     709                result.add(new WayInPath(bestWay, bestWayReverse)); 
     710                headNode = bestWayReverse ? bestWay.firstNode() : bestWay.lastNode(); 
     711                prevNode = bestWayReverse ? bestWay.getNode(1) : bestWay.getNode(bestWay.getNodesCount() - 2); 
     712            } 
    562713        } 
    563714 
    564         return innerWays; 
     715        return result; 
    565716    } 
    566717 
    567     // Polygon only supports int coordinates, so convert them 
    568     private int latlonToXY(double val) { 
    569         return (int)Math.round(val*1000000); 
     718    /** 
     719     * Tests if given point is to the right side of path consisting of 3 points. 
     720     * @param lineP1 first point in path 
     721     * @param lineP2 second point in path 
     722     * @param lineP3 third point in path 
     723     * @param testPoint 
     724     * @return true if to the right side, false otherwise 
     725     */ 
     726    public static boolean isToTheRightSideOfLine(Node lineP1, Node lineP2, Node lineP3, Node testPoint) 
     727    { 
     728        boolean pathBendToRight = angleIsClockwise(lineP1, lineP2, lineP3); 
     729        boolean rightOfSeg1 = angleIsClockwise(lineP1, lineP2, testPoint); 
     730        boolean rightOfSeg2 = angleIsClockwise(lineP2, lineP3, testPoint); 
     731 
     732        if (pathBendToRight) 
     733            return rightOfSeg1 && rightOfSeg2; 
     734        else 
     735            return !(!rightOfSeg1 && !rightOfSeg2); 
    570736    } 
    571737 
    572738    /** 
    573      * Finds all ways that contain the given node. 
    574      * @param Collection<Way> A list to which matching ways will be added 
    575      * @param Collection<Way> A list of ways to check 
    576      * @param Node The node the ways should be checked against 
     739     * This method tests if secondNode is clockwise to first node. 
     740     * @param commonNode starting point for both vectors 
     741     * @param firstNode first vector end node 
     742     * @param secondNode second vector end node 
     743     * @return true if first vector is clockwise before second vector. 
    577744     */ 
    578     private void getWaysByNode(Collection<Way> innerWays, Collection<Way> w, Node n) { 
    579         for(Way way : w) { 
    580             if(!(way).containsNode(n)) { 
     745    public static boolean angleIsClockwise(Node commonNode, Node firstNode, Node secondNode) 
     746    { 
     747        double dla1 = (firstNode.getCoor().lat() - commonNode.getCoor().lat()); 
     748        double dla2 = (secondNode.getCoor().lat() - commonNode.getCoor().lat()); 
     749        double dlo1 = (firstNode.getCoor().lon() - commonNode.getCoor().lon()); 
     750        double dlo2 = (secondNode.getCoor().lon() - commonNode.getCoor().lon()); 
     751 
     752        return dla1 * dlo2 - dlo1 * dla2 > 0; 
     753    } 
     754 
     755 
     756    /** 
     757     * Tests if point is inside a polygon. The polygon can be self-intersecting. In such case the contains function works in xor-like manner. 
     758     * @param polygonNodes list of nodes from polygon path. 
     759     * @param point the point to test 
     760     * @return true if the point is inside polygon. 
     761     * FIXME: this should probably be moved to tools.. 
     762     */ 
     763    public static boolean nodeInsidePolygon(ArrayList<Node> polygonNodes, Node point) 
     764    { 
     765        if (polygonNodes.size() < 3) 
     766            return false; 
     767 
     768        boolean inside = false; 
     769        Node p1, p2; 
     770 
     771        //iterate each side of the polygon, start with the last segment 
     772        Node oldPoint = polygonNodes.get(polygonNodes.size() - 1); 
     773 
     774        for(Node newPoint: polygonNodes) 
     775        { 
     776            //skip duplicate points 
     777            if (newPoint.equals(oldPoint)) { 
    581778                continue; 
    582779            } 
    583             if(!innerWays.contains(way)) { 
    584                 innerWays.add(way); // Will need this later for multigons 
     780 
     781            //order points so p1.lat <= p2.lat; 
     782            if (newPoint.getCoor().lat() > oldPoint.getCoor().lat()) 
     783            { 
     784                p1 = oldPoint; 
     785                p2 = newPoint; 
    585786            } 
     787            else 
     788            { 
     789                p1 = newPoint; 
     790                p2 = oldPoint; 
     791            } 
     792 
     793            //test if the line is crossed and if so invert the inside flag. 
     794            if ((newPoint.getCoor().lat() < point.getCoor().lat()) == (point.getCoor().lat() <= oldPoint.getCoor().lat()) 
     795                    && (point.getCoor().lon() - p1.getCoor().lon()) * (p2.getCoor().lat() - p1.getCoor().lat()) 
     796                    < (p2.getCoor().lon() - p1.getCoor().lon()) * (point.getCoor().lat() - p1.getCoor().lat())) 
     797            { 
     798                inside = !inside; 
     799            } 
     800 
     801            oldPoint = newPoint; 
    586802        } 
     803 
     804        return inside; 
    587805    } 
    588806 
     807 
     808 
     809 
    589810    /** 
    590      * Joins the two outer ways and deletes all short ways that can't be part of a multipolygon anyway 
    591      * @param Collection<OsmPrimitive> The list of all ways that belong to that multigon 
    592      * @param Collection<Way> The list of inner ways that belong to that multigon 
     811     * Joins the outer ways and deletes all short ways that can't be part of a multipolygon anyway. 
     812     * @param Collection<Way> The list of outer ways that belong to that multigon. 
    593813     * @return Way The newly created outer way 
    594814     */ 
    595     private Way joinOuterWays(Collection<Way> multigonWays, Collection<Way> innerWays) { 
    596         ArrayList<Way> join = new ArrayList<Way>(); 
    597         for(Way w: multigonWays) { 
    598             // Skip inner ways 
    599             if(innerWays.contains(w)) { 
    600                 continue; 
    601             } 
     815    private Way joinOuterWays(ArrayList<WayInPath> outerWays) { 
    602816 
    603             if(w.getNodesCount() <= 2) { 
    604                 cmds.add(new DeleteCommand(w)); 
    605             } else { 
    606                 join.add(w); 
     817        //leave original orientation, if all paths are reverse. 
     818        boolean allReverse = true; 
     819        for(WayInPath way: outerWays){ 
     820            allReverse &= way.reverse; 
     821        } 
     822 
     823        if (allReverse){ 
     824            for(WayInPath way: outerWays){ 
     825                way.reverse = !way.reverse; 
    607826            } 
    608827        } 
    609828 
     829 
    610830        commitCommands(marktr("Join Areas: Remove Short Ways")); 
    611         Way joinedWay = joinWays(join); 
     831        Way joinedWay = joinOrientedWays(outerWays); 
    612832        if (joinedWay != null) 
    613833            return closeWay(joinedWay); 
    614834        else 
     
    632852    } 
    633853 
    634854    /** 
     855     * Joins a list of ways (using CombineWayAction and ReverseWayAction as specified in WayInPath) 
     856     * @param ArrayList<Way> The list of ways to join and reverse 
     857     * @return Way The newly created way 
     858     */ 
     859    private Way joinOrientedWays(ArrayList<WayInPath> ways) { 
     860        if(ways.size() < 2) return ways.get(0).way; 
     861 
     862        // This will turn ways so all of them point in the same direction and CombineAction won't bug 
     863        // the user about this. 
     864 
     865        List<Way> actionWays = new ArrayList<Way>(ways.size()); 
     866 
     867        for(WayInPath way : ways) { 
     868            actionWays.add(way.way); 
     869 
     870            if (way.reverse) 
     871            { 
     872                Main.main.getCurrentDataSet().setSelected(way.way); 
     873                new ReverseWayAction().actionPerformed(null); 
     874                cmdsCount++; 
     875            } 
     876        } 
     877 
     878        Way result = new CombineWayAction().combineWays(actionWays); 
     879 
     880        if(result != null) { 
     881            cmdsCount++; 
     882        } 
     883        return result; 
     884    } 
     885 
     886    /** 
    635887     * Joins a list of ways (using CombineWayAction and ReverseWayAction if necessary to quiet the former) 
    636888     * @param ArrayList<Way> The list of ways to join 
    637889     * @return Way The newly created way 
     
    661913        return a; 
    662914    } 
    663915 
     916 
    664917    /** 
    665918     * Finds all ways that may be part of a multipolygon relation and removes them from the given list. 
    666919     * It will automatically combine "good" ways 
     
    9801233    protected void updateEnabledState(Collection<? extends OsmPrimitive> selection) { 
    9811234        setEnabled(selection != null && !selection.isEmpty()); 
    9821235    } 
    983 } 
     1236 
     1237} 
     1238 No newline at end of file 
  • test/unit/actions/JoinAreasActionTest.java

     
     1// License: GPL. For details, see LICENSE file. 
     2package actions; 
     3 
     4import org.junit.Assert; 
     5import org.junit.Test; 
     6import org.openstreetmap.josm.actions.JoinAreasAction; 
     7import org.openstreetmap.josm.data.coor.LatLon; 
     8import org.openstreetmap.josm.data.osm.Node; 
     9 
     10 
     11public class JoinAreasActionTest { 
     12 
     13    private Node makeNode(double lat, double lon) 
     14    { 
     15        Node node = new Node(new LatLon(lat, lon)); 
     16        return node; 
     17    } 
     18 
     19    @Test 
     20    public void testAngleIsClockwise() 
     21    { 
     22        Assert.assertTrue(JoinAreasAction.angleIsClockwise(makeNode(0,0), makeNode(1,1), makeNode(0,1))); 
     23        Assert.assertTrue(JoinAreasAction.angleIsClockwise(makeNode(1,1), makeNode(0,1), makeNode(0,0))); 
     24        Assert.assertTrue(!JoinAreasAction.angleIsClockwise(makeNode(1,1), makeNode(0,1), makeNode(1,0))); 
     25    } 
     26 
     27    @Test 
     28    public void testisToTheRightSideOfLine() 
     29    { 
     30        Assert.assertTrue(JoinAreasAction.isToTheRightSideOfLine(makeNode(0,0), makeNode(1,1), makeNode(0,1), makeNode(0, 0.5))); 
     31        Assert.assertTrue(!JoinAreasAction.isToTheRightSideOfLine(makeNode(0,0), makeNode(1,1), makeNode(0,1), makeNode(1, 0))); 
     32        Assert.assertTrue(!JoinAreasAction.isToTheRightSideOfLine(makeNode(1,1), makeNode(0,1), makeNode(1,0), makeNode(0,0))); 
     33        Assert.assertTrue(JoinAreasAction.isToTheRightSideOfLine(makeNode(1,1), makeNode(0,1), makeNode(1,0), makeNode(2, 0))); 
     34    } 
     35 
     36}