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

File osm-join-areas-4.patch, 24.7 KB (added by extropy, 14 years ago)
  • 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 final 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);
    607             }
    608         }
    609 
    610817        commitCommands(marktr("Join Areas: Remove Short Ways"));
    611         Way joinedWay = joinWays(join);
     818        Way joinedWay = joinOrientedWays(outerWays);
    612819        if (joinedWay != null)
    613820            return closeWay(joinedWay);
    614821        else
     
    632839    }
    633840
    634841    /**
     842     * Joins a list of ways (using CombineWayAction and ReverseWayAction as specified in WayInPath)
     843     * @param ArrayList<Way> The list of ways to join and reverse
     844     * @return Way The newly created way
     845     */
     846    private Way joinOrientedWays(ArrayList<WayInPath> ways) {
     847        if(ways.size() < 2) return ways.get(0).way;
     848
     849        // This will turn ways so all of them point in the same direction and CombineAction won't bug
     850        // the user about this.
     851
     852        List<Way> actionWays = new ArrayList<Way>(ways.size());
     853
     854        for(WayInPath way : ways) {
     855            actionWays.add(way.way);
     856
     857            if (way.reverse)
     858            {
     859                Main.main.getCurrentDataSet().setSelected(way.way);
     860                new ReverseWayAction().actionPerformed(null);
     861                cmdsCount++;
     862            }
     863        }
     864
     865        Way result = new CombineWayAction().combineWays(actionWays);
     866
     867        if(result != null) {
     868            cmdsCount++;
     869        }
     870        return result;
     871    }
     872
     873    /**
    635874     * Joins a list of ways (using CombineWayAction and ReverseWayAction if necessary to quiet the former)
    636875     * @param ArrayList<Way> The list of ways to join
    637876     * @return Way The newly created way
     
    661900        return a;
    662901    }
    663902
     903
    664904    /**
    665905     * Finds all ways that may be part of a multipolygon relation and removes them from the given list.
    666906     * It will automatically combine "good" ways
     
    9801220    protected void updateEnabledState(Collection<? extends OsmPrimitive> selection) {
    9811221        setEnabled(selection != null && !selection.isEmpty());
    9821222    }
    983 }
     1223
     1224}
     1225 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}